mirror of
https://github.com/KoalaSat/nostros.git
synced 2024-09-28 22:30:41 +00:00
Group Page
This commit is contained in:
parent
187e13808a
commit
2a5a35031f
18
.github/ISSUE_TEMPLATE/bug_report.md
vendored
18
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@ -4,7 +4,6 @@ about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
@ -12,6 +11,7 @@ A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
@ -24,15 +24,17 @@ A clear and concise description of what you expected to happen.
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Desktop (please complete the following information):**
|
||||
- OS: [e.g. iOS]
|
||||
- Browser [e.g. chrome, safari]
|
||||
- Version [e.g. 22]
|
||||
|
||||
- OS: [e.g. iOS]
|
||||
- Browser [e.g. chrome, safari]
|
||||
- Version [e.g. 22]
|
||||
|
||||
**Smartphone (please complete the following information):**
|
||||
- Device: [e.g. iPhone6]
|
||||
- OS: [e.g. iOS8.1]
|
||||
- Browser [e.g. stock browser, safari]
|
||||
- Version [e.g. 22]
|
||||
|
||||
- Device: [e.g. iPhone6]
|
||||
- OS: [e.g. iOS8.1]
|
||||
- Browser [e.g. stock browser, safari]
|
||||
- Version [e.g. 22]
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
|
1
.github/ISSUE_TEMPLATE/feature_request.md
vendored
1
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@ -4,7 +4,6 @@ about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
|
@ -4,7 +4,6 @@ import android.annotation.SuppressLint;
|
||||
import android.content.ContentValues;
|
||||
import android.database.Cursor;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.util.Log;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
@ -17,7 +16,6 @@ import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
@ -66,6 +64,14 @@ public class Event {
|
||||
saveReaction(database);
|
||||
} else if (kind.equals("40")) {
|
||||
saveGroup(database);
|
||||
} else if (kind.equals("41")) {
|
||||
updateGroup(database);
|
||||
} else if (kind.equals("42")) {
|
||||
saveGroupMessage(database);
|
||||
} else if (kind.equals("42")) {
|
||||
hideGroupMessage(database);
|
||||
} else if (kind.equals("44")) {
|
||||
blockUser(database);
|
||||
}
|
||||
} catch (JSONException e) {
|
||||
e.printStackTrace();
|
||||
@ -203,6 +209,61 @@ public class Event {
|
||||
database.replace("nostros_notes", null, values);
|
||||
}
|
||||
|
||||
protected void updateGroup(SQLiteDatabase database) throws JSONException {
|
||||
JSONObject groupContent = new JSONObject(content);
|
||||
JSONArray eTags = filterTags("e");
|
||||
String groupId = eTags.getJSONArray(0).getString(1);
|
||||
String query = "SELECT created_at, pubkey FROM nostros_groups WHERE id = ?";
|
||||
@SuppressLint("Recycle") Cursor cursor = database.rawQuery(query, new String[] {groupId});
|
||||
|
||||
if (cursor.moveToFirst() && created_at > cursor.getInt(0) && pubkey.equals(cursor.getString(1))) {
|
||||
ContentValues values = new ContentValues();
|
||||
values.put("name", groupContent.optString("name"));
|
||||
values.put("about", groupContent.optString("about"));
|
||||
values.put("picture", groupContent.optString("picture"));
|
||||
|
||||
String whereClause = "id = ?";
|
||||
String[] whereArgs = new String[] {
|
||||
groupId
|
||||
};
|
||||
database.update("nostros_groups", values, whereClause, whereArgs);
|
||||
}
|
||||
}
|
||||
|
||||
protected void blockUser(SQLiteDatabase database) throws JSONException {
|
||||
JSONArray pTags = filterTags("p");
|
||||
String groupId = pTags.getJSONArray(0).getString(1);
|
||||
String query = "SELECT id FROM nostros_users WHERE id = ?";
|
||||
@SuppressLint("Recycle") Cursor cursor = database.rawQuery(query, new String[] {groupId});
|
||||
|
||||
if (cursor.getCount() == 0) {
|
||||
ContentValues values = new ContentValues();
|
||||
values.put("muted_groups", 1);
|
||||
String whereClause = "id = ?";
|
||||
String[] whereArgs = new String[] {
|
||||
groupId
|
||||
};
|
||||
database.update("nostros_users", values, whereClause, whereArgs);
|
||||
}
|
||||
}
|
||||
|
||||
protected void hideGroupMessage(SQLiteDatabase database) throws JSONException {
|
||||
JSONArray eTags = filterTags("e");
|
||||
String groupId = eTags.getJSONArray(0).getString(1);
|
||||
String query = "SELECT id FROM nostros_group_messages WHERE id = ?";
|
||||
@SuppressLint("Recycle") Cursor cursor = database.rawQuery(query, new String[] {groupId});
|
||||
|
||||
if (cursor.getCount() == 0) {
|
||||
ContentValues values = new ContentValues();
|
||||
values.put("hidden", 1);
|
||||
String whereClause = "id = ?";
|
||||
String[] whereArgs = new String[] {
|
||||
groupId
|
||||
};
|
||||
database.update("nostros_group_messages", values, whereClause, whereArgs);
|
||||
}
|
||||
}
|
||||
|
||||
protected void saveGroup(SQLiteDatabase database) throws JSONException {
|
||||
JSONObject groupContent = new JSONObject(content);
|
||||
|
||||
@ -217,7 +278,24 @@ public class Event {
|
||||
values.put("name", groupContent.optString("name"));
|
||||
values.put("about", groupContent.optString("about"));
|
||||
values.put("picture", groupContent.optString("picture"));
|
||||
database.replace("nostros_groups", null, values);
|
||||
database.insert("nostros_groups", null, values);
|
||||
}
|
||||
|
||||
protected void saveGroupMessage(SQLiteDatabase database) throws JSONException {
|
||||
JSONArray eTags = filterTags("e");
|
||||
String groupId = eTags.getJSONArray(0).getString(1);
|
||||
|
||||
ContentValues values = new ContentValues();
|
||||
values.put("id", id);
|
||||
values.put("content", content);
|
||||
values.put("created_at", created_at);
|
||||
values.put("kind", kind);
|
||||
values.put("pubkey", pubkey);
|
||||
values.put("sig", sig);
|
||||
values.put("tags", tags.toString());
|
||||
values.put("group_id", groupId);
|
||||
|
||||
database.insert("nostros_group_messages", null, values);
|
||||
}
|
||||
|
||||
protected void saveDirectMessage(SQLiteDatabase database) throws JSONException {
|
||||
@ -230,7 +308,7 @@ public class Event {
|
||||
|
||||
ContentValues values = new ContentValues();
|
||||
values.put("id", id);
|
||||
values.put("content", content.replace("'", "''"));
|
||||
values.put("content", content);
|
||||
values.put("created_at", created_at);
|
||||
values.put("kind", kind);
|
||||
values.put("pubkey", pubkey);
|
||||
@ -256,7 +334,7 @@ public class Event {
|
||||
|
||||
ContentValues values = new ContentValues();
|
||||
values.put("id", id);
|
||||
values.put("content", content.replace("'", "''"));
|
||||
values.put("content", content);
|
||||
values.put("created_at", created_at);
|
||||
values.put("kind", kind);
|
||||
values.put("pubkey", pubkey);
|
||||
|
@ -142,6 +142,7 @@ public class DatabaseModule {
|
||||
database.execSQL("ALTER TABLE nostros_relays ADD COLUMN manual INT DEFAULT 1;");
|
||||
} catch (SQLException e) { }
|
||||
try {
|
||||
database.execSQL("ALTER TABLE nostros_users ADD COLUMN muted_groups INT DEFAULT 0;");
|
||||
database.execSQL("CREATE TABLE IF NOT EXISTS nostros_groups(\n" +
|
||||
" id TEXT PRIMARY KEY NOT NULL, \n" +
|
||||
" content TEXT NOT NULL,\n" +
|
||||
@ -154,7 +155,18 @@ public class DatabaseModule {
|
||||
" about TEXT NOT NULL,\n" +
|
||||
" picture TEXT NOT NULL\n" +
|
||||
" );");
|
||||
database.execSQL("CREATE INDEX nostros_groups_pubkey_index ON nostros_groups(pubkey);");
|
||||
database.execSQL("CREATE TABLE IF NOT EXISTS nostros_group_messages(\n" +
|
||||
" id TEXT PRIMARY KEY NOT NULL, \n" +
|
||||
" content TEXT NOT NULL,\n" +
|
||||
" created_at INT NOT NULL,\n" +
|
||||
" kind INT NOT NULL,\n" +
|
||||
" pubkey TEXT NOT NULL,\n" +
|
||||
" sig TEXT NOT NULL,\n" +
|
||||
" tags TEXT NOT NULL,\n" +
|
||||
" group_id TEXT NOT NULL,\n" +
|
||||
" hidden INT DEFAULT 0\n" +
|
||||
" );");
|
||||
database.execSQL("CREATE INDEX nostros_group_messages_group_id_index ON nostros_group_messages(group_id, created_at);");
|
||||
} catch (SQLException e) { }
|
||||
}
|
||||
|
||||
|
@ -1,27 +1,89 @@
|
||||
import React, { useContext, useEffect, useState } from 'react'
|
||||
import { StyleSheet, View } from 'react-native'
|
||||
import { Clipboard, StyleSheet, View } from 'react-native'
|
||||
import RBSheet from 'react-native-raw-bottom-sheet'
|
||||
import { Avatar as PaperAvatar, TouchableRipple, useTheme } from 'react-native-paper'
|
||||
import { getGroup, Group } from '../../Functions/DatabaseFunctions/Groups'
|
||||
import {
|
||||
Avatar as PaperAvatar,
|
||||
Button,
|
||||
Text,
|
||||
TextInput,
|
||||
TouchableRipple,
|
||||
useTheme,
|
||||
} from 'react-native-paper'
|
||||
import { deleteGroup, getGroup, Group } from '../../Functions/DatabaseFunctions/Groups'
|
||||
import { AppContext } from '../../Contexts/AppContext'
|
||||
import { validImageUrl } from '../../Functions/NativeFunctions'
|
||||
import FastImage from 'react-native-fast-image'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons'
|
||||
import UploadImage from '../UploadImage'
|
||||
import { UserContext } from '../../Contexts/UserContext'
|
||||
import { getUnixTime } from 'date-fns'
|
||||
import { Kind } from 'nostr-tools'
|
||||
import { RelayPoolContext } from '../../Contexts/RelayPoolContext'
|
||||
import { Event } from '../../lib/nostr/Events'
|
||||
import { goBack } from '../../lib/Navigation'
|
||||
|
||||
interface GroupHeaderIconProps {
|
||||
groupId: string
|
||||
}
|
||||
|
||||
export const GroupHeaderIcon: React.FC<GroupHeaderIconProps> = ({ groupId }) => {
|
||||
const { t } = useTranslation('common')
|
||||
const { database } = useContext(AppContext)
|
||||
const { publicKey } = useContext(UserContext)
|
||||
const { relayPool, lastEventId } = useContext(RelayPoolContext)
|
||||
const theme = useTheme()
|
||||
const [group, setGroup] = useState<Group>()
|
||||
const [newGroupName, setNewGroupName] = useState<string>()
|
||||
const [newGroupDescription, setNewGroupDescription] = useState<string>()
|
||||
const [newGroupPicture, setNewGroupPicture] = useState<string>()
|
||||
const [startUpload, setStartUpload] = useState<boolean>(false)
|
||||
const [uploadingFile, setUploadingFile] = useState<boolean>(false)
|
||||
const bottomSheetActionsGroupRef = React.useRef<RBSheet>(null)
|
||||
const bottomSheetEditGroupRef = React.useRef<RBSheet>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (database && groupId) {
|
||||
getGroup(database, groupId).then(setGroup)
|
||||
getGroup(database, groupId).then((result) => {
|
||||
setGroup(result)
|
||||
setNewGroupName(result.name)
|
||||
setNewGroupDescription(result.about)
|
||||
setNewGroupPicture(result.picture)
|
||||
})
|
||||
}
|
||||
}, [])
|
||||
}, [lastEventId])
|
||||
|
||||
const pastePicture: () => void = () => {
|
||||
Clipboard.getString().then((value) => {
|
||||
setNewGroupPicture(value ?? '')
|
||||
})
|
||||
}
|
||||
|
||||
const onDeleteGroup: () => void = () => {
|
||||
if (database && group?.id) {
|
||||
deleteGroup(database, group?.id)
|
||||
goBack()
|
||||
bottomSheetActionsGroupRef.current?.close()
|
||||
}
|
||||
}
|
||||
|
||||
const updateGroup: () => void = () => {
|
||||
if (newGroupName && publicKey && group?.id) {
|
||||
const event: Event = {
|
||||
content: JSON.stringify({
|
||||
name: newGroupName,
|
||||
about: newGroupDescription,
|
||||
picture: newGroupPicture,
|
||||
}),
|
||||
created_at: getUnixTime(new Date()),
|
||||
kind: Kind.ChannelMetadata,
|
||||
pubkey: publicKey,
|
||||
tags: [['e', group?.id, '']],
|
||||
}
|
||||
relayPool?.sendEvent(event)
|
||||
bottomSheetEditGroupRef.current?.close()
|
||||
}
|
||||
}
|
||||
|
||||
const bottomSheetStyles = React.useMemo(() => {
|
||||
return {
|
||||
@ -40,8 +102,14 @@ export const GroupHeaderIcon: React.FC<GroupHeaderIconProps> = ({ groupId }) =>
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<TouchableRipple onPress={() => {}}>
|
||||
{validImageUrl(group?.picture) ? (
|
||||
<TouchableRipple
|
||||
onPress={() =>
|
||||
group?.pubkey === publicKey
|
||||
? bottomSheetEditGroupRef.current?.open()
|
||||
: bottomSheetActionsGroupRef.current?.open()
|
||||
}
|
||||
>
|
||||
{validImageUrl(group?.picture) ? (
|
||||
<FastImage
|
||||
style={[
|
||||
{
|
||||
@ -61,6 +129,89 @@ export const GroupHeaderIcon: React.FC<GroupHeaderIconProps> = ({ groupId }) =>
|
||||
<PaperAvatar.Text size={35} label={group?.name ?? group?.id ?? ''} />
|
||||
)}
|
||||
</TouchableRipple>
|
||||
<RBSheet
|
||||
ref={bottomSheetActionsGroupRef}
|
||||
closeOnDragDown={true}
|
||||
customStyles={bottomSheetStyles}
|
||||
>
|
||||
<View>
|
||||
<Button mode='contained' onPress={onDeleteGroup}>
|
||||
{t('groupsFeed.delete')}
|
||||
</Button>
|
||||
</View>
|
||||
</RBSheet>
|
||||
<RBSheet
|
||||
ref={bottomSheetEditGroupRef}
|
||||
closeOnDragDown={true}
|
||||
customStyles={bottomSheetStyles}
|
||||
>
|
||||
<View>
|
||||
<Text style={styles.input} variant='titleLarge'>
|
||||
{t('groupsFeed.updateTitle')}
|
||||
</Text>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
mode='outlined'
|
||||
label={t('groupsFeed.newGroupName') ?? ''}
|
||||
onChangeText={setNewGroupName}
|
||||
value={newGroupName}
|
||||
/>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
multiline
|
||||
mode='outlined'
|
||||
label={t('groupsFeed.newGroupDescription') ?? ''}
|
||||
onChangeText={setNewGroupDescription}
|
||||
value={newGroupDescription}
|
||||
/>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
mode='outlined'
|
||||
label={t('groupsFeed.newGroupPicture') ?? ''}
|
||||
onChangeText={setNewGroupPicture}
|
||||
value={newGroupPicture}
|
||||
left={
|
||||
<TextInput.Icon
|
||||
icon={() => (
|
||||
<MaterialCommunityIcons
|
||||
name='image-outline'
|
||||
size={25}
|
||||
color={theme.colors.onPrimaryContainer}
|
||||
/>
|
||||
)}
|
||||
onPress={() => setStartUpload(true)}
|
||||
/>
|
||||
}
|
||||
right={
|
||||
<TextInput.Icon
|
||||
icon='content-paste'
|
||||
onPress={pastePicture}
|
||||
forceTextInputFocus={false}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Button
|
||||
mode='contained'
|
||||
disabled={!newGroupName}
|
||||
onPress={() => updateGroup()}
|
||||
style={styles.input}
|
||||
>
|
||||
{t('groupsFeed.groupUpdate')}
|
||||
</Button>
|
||||
<Button mode='outlined' onPress={onDeleteGroup}>
|
||||
{t('groupsFeed.delete')}
|
||||
</Button>
|
||||
<UploadImage
|
||||
startUpload={startUpload}
|
||||
setImageUri={(imageUri) => {
|
||||
setNewGroupPicture(imageUri)
|
||||
setStartUpload(false)
|
||||
}}
|
||||
uploadingFile={uploadingFile}
|
||||
setUploadingFile={setUploadingFile}
|
||||
/>
|
||||
</View>
|
||||
</RBSheet>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@ -69,7 +220,9 @@ const styles = StyleSheet.create({
|
||||
container: {
|
||||
paddingRight: 8,
|
||||
},
|
||||
input: {
|
||||
marginBottom: 16,
|
||||
},
|
||||
})
|
||||
|
||||
export default GroupHeaderIcon
|
||||
|
||||
|
@ -473,7 +473,7 @@ const styles = StyleSheet.create({
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignContent: 'center',
|
||||
paddingBottom: 16
|
||||
paddingBottom: 16,
|
||||
},
|
||||
userBlockedWrapper: {
|
||||
flexDirection: 'row',
|
||||
|
@ -5,6 +5,7 @@ import { IconButton, List, Snackbar, Text, useTheme } from 'react-native-paper'
|
||||
import { AppContext } from '../../Contexts/AppContext'
|
||||
import { RelayPoolContext } from '../../Contexts/RelayPoolContext'
|
||||
import { UserContext } from '../../Contexts/UserContext'
|
||||
import { Event } from '../../lib/nostr/Events'
|
||||
import {
|
||||
addUser,
|
||||
getUser,
|
||||
@ -21,6 +22,10 @@ import { getUserRelays, NoteRelay } from '../../Functions/DatabaseFunctions/Note
|
||||
import { relayToColor } from '../../Functions/NativeFunctions'
|
||||
import { Relay } from '../../Functions/DatabaseFunctions/Relays'
|
||||
import ProfileShare from '../ProfileShare'
|
||||
import { deleteGroupMessages } from '../../Functions/DatabaseFunctions/Groups'
|
||||
import { getUnixTime } from 'date-fns'
|
||||
import { Kind } from 'nostr-tools'
|
||||
import { ScrollView } from 'react-native-gesture-handler'
|
||||
|
||||
interface ProfileActionsProps {
|
||||
user: User
|
||||
@ -78,12 +83,25 @@ export const ProfileActions: React.FC<ProfileActionsProps> = ({
|
||||
}
|
||||
|
||||
const onChangeBlockUser: () => void = () => {
|
||||
if (database) {
|
||||
if (database && publicKey) {
|
||||
addUser(user.id, database).then(() => {
|
||||
updateUserBlock(user.id, database, !isBlocked).then(() => {
|
||||
loadUser()
|
||||
setShowNotificationRelay(isBlocked ? 'userUnblocked' : 'userBlocked')
|
||||
})
|
||||
if (!isBlocked) {
|
||||
const event: Event = {
|
||||
content: '',
|
||||
created_at: getUnixTime(new Date()),
|
||||
kind: Kind.ChannelMuteUser,
|
||||
pubkey: publicKey,
|
||||
tags: [['p', user.id]],
|
||||
}
|
||||
relayPool?.sendEvent(event)
|
||||
deleteGroupMessages(database, user.id).then(() => {
|
||||
onActionDone()
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -231,7 +249,7 @@ export const ProfileActions: React.FC<ProfileActionsProps> = ({
|
||||
<View>
|
||||
<Text variant='titleLarge'>{t('profileCard.relaysTitle')}</Text>
|
||||
<Text variant='bodyMedium'>
|
||||
{t('profileCard.relaysDescription', { username: username(user) })}
|
||||
{t('profilePage.relaysDescription', { username: username(user) })}
|
||||
</Text>
|
||||
<List.Item
|
||||
title={t('relaysPage.relayName')}
|
||||
@ -241,11 +259,13 @@ export const ProfileActions: React.FC<ProfileActionsProps> = ({
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
<FlatList
|
||||
showsVerticalScrollIndicator={false}
|
||||
data={userRelays}
|
||||
renderItem={renderRelayItem}
|
||||
/>
|
||||
<ScrollView>
|
||||
<FlatList
|
||||
showsVerticalScrollIndicator={false}
|
||||
data={userRelays}
|
||||
renderItem={renderRelayItem}
|
||||
/>
|
||||
</ScrollView>
|
||||
</View>
|
||||
{showNotificationRelay && (
|
||||
<Snackbar
|
||||
|
@ -79,7 +79,11 @@ export const ProfileCard: React.FC<ProfileCardProps> = ({ bottomSheetRef, showIm
|
||||
<Divider />
|
||||
{user && (
|
||||
<View style={styles.profileActions}>
|
||||
<ProfileActions user={user} setUser={setUser} onActionDone={() => bottomSheetRef.current?.close()}/>
|
||||
<ProfileActions
|
||||
user={user}
|
||||
setUser={setUser}
|
||||
onActionDone={() => bottomSheetRef.current?.close()}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
{showNotification && (
|
||||
@ -176,7 +180,7 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
arrow: {
|
||||
alignContent: 'center',
|
||||
justifyContent: 'center'
|
||||
justifyContent: 'center',
|
||||
},
|
||||
})
|
||||
|
||||
|
@ -104,10 +104,11 @@ export const RelayCard: React.FC<RelayCardProps> = ({ url, bottomSheetRef }) =>
|
||||
Accept: 'application/nostr+json',
|
||||
}
|
||||
axios
|
||||
.get('http://' + uri, {
|
||||
.get('http://' + uri.replace('wss://', '').replace('ws://', ''), {
|
||||
headers,
|
||||
})
|
||||
.then((response) => {
|
||||
console.log(response)
|
||||
setRelayInfo(response.data)
|
||||
})
|
||||
.catch((e) => {
|
||||
|
@ -6,6 +6,7 @@ export interface DirectMessage extends Event {
|
||||
conversation_id: string
|
||||
read: boolean
|
||||
pending: boolean
|
||||
valid_nip05: boolean
|
||||
}
|
||||
|
||||
const databaseToEntity: (object: any) => DirectMessage = (object = {}) => {
|
||||
|
@ -10,11 +10,23 @@ export interface Group extends Event {
|
||||
valid_nip05: boolean
|
||||
}
|
||||
|
||||
const databaseToEntity: (object: any) => Group = (object = {}) => {
|
||||
export interface GroupMessage extends Event {
|
||||
pending: boolean
|
||||
name: string
|
||||
picture?: string
|
||||
valid_nip05?: boolean
|
||||
}
|
||||
|
||||
const databaseToGroup: (object: any) => Group = (object = {}) => {
|
||||
object.tags = object.tags ? JSON.parse(object.tags) : []
|
||||
return object as Group
|
||||
}
|
||||
|
||||
const databaseToGroupMessage: (object: any) => GroupMessage = (object = {}) => {
|
||||
object.tags = object.tags ? JSON.parse(object.tags) : []
|
||||
return object as GroupMessage
|
||||
}
|
||||
|
||||
export const updateConversationRead: (
|
||||
conversationId: string,
|
||||
db: QuickSQLiteConnection,
|
||||
@ -30,9 +42,7 @@ export const updateAllRead: (db: QuickSQLiteConnection) => Promise<QueryResult |
|
||||
return db.execute(userQuery, [1])
|
||||
}
|
||||
|
||||
export const getGroups: (
|
||||
db: QuickSQLiteConnection
|
||||
) => Promise<Group[]> = async (db) => {
|
||||
export const getGroups: (db: QuickSQLiteConnection) => Promise<Group[]> = async (db) => {
|
||||
const groupsQuery = `
|
||||
SELECT
|
||||
nostros_groups.*, nostros_users.name as user_name, nostros_users.valid_nip05
|
||||
@ -43,15 +53,15 @@ export const getGroups: (
|
||||
`
|
||||
const resultSet = await db.execute(groupsQuery)
|
||||
const items: object[] = getItems(resultSet)
|
||||
const notes: Group[] = items.map((object) => databaseToEntity(object))
|
||||
const notes: Group[] = items.map((object) => databaseToGroup(object))
|
||||
|
||||
return notes
|
||||
}
|
||||
|
||||
export const getGroup: (
|
||||
db: QuickSQLiteConnection,
|
||||
groupId: string
|
||||
) => Promise<Group> = async (db, groupId) => {
|
||||
export const getGroup: (db: QuickSQLiteConnection, groupId: string) => Promise<Group> = async (
|
||||
db,
|
||||
groupId,
|
||||
) => {
|
||||
const groupsQuery = `
|
||||
SELECT
|
||||
*
|
||||
@ -62,7 +72,65 @@ export const getGroup: (
|
||||
`
|
||||
const resultSet = await db.execute(groupsQuery, [groupId])
|
||||
const items: object[] = getItems(resultSet)
|
||||
const group: Group = databaseToEntity(items[0])
|
||||
const group: Group = databaseToGroup(items[0])
|
||||
|
||||
return group
|
||||
}
|
||||
|
||||
export const getGroupMessages: (
|
||||
db: QuickSQLiteConnection,
|
||||
groupId: string,
|
||||
options: {
|
||||
order?: 'DESC' | 'ASC'
|
||||
limit?: number
|
||||
},
|
||||
) => Promise<GroupMessage[]> = async (db, groupId, { order = 'DESC', limit }) => {
|
||||
let notesQuery = `
|
||||
SELECT
|
||||
nostros_group_messages.*, nostros_users.name, nostros_users.picture, nostros_users.valid_nip05
|
||||
FROM
|
||||
nostros_group_messages
|
||||
LEFT JOIN
|
||||
nostros_users ON nostros_users.id = nostros_group_messages.pubkey
|
||||
WHERE group_id = "${groupId}"
|
||||
AND nostros_users.muted_groups < 1
|
||||
ORDER BY created_at ${order}
|
||||
`
|
||||
if (limit) {
|
||||
notesQuery += `LIMIT ${limit}`
|
||||
}
|
||||
|
||||
const resultSet = await db.execute(notesQuery)
|
||||
const items: object[] = getItems(resultSet)
|
||||
const messages: GroupMessage[] = items.map((object) => databaseToGroupMessage(object))
|
||||
|
||||
return messages
|
||||
}
|
||||
|
||||
export const deleteGroupMessages: (
|
||||
db: QuickSQLiteConnection,
|
||||
pubkey: string,
|
||||
) => Promise<QueryResult> = async (db, pubkey) => {
|
||||
const deleteQuery = `
|
||||
DELETE FROM nostros_group_messages
|
||||
WHERE pubkey = ?
|
||||
`
|
||||
|
||||
return db.execute(deleteQuery, [pubkey])
|
||||
}
|
||||
|
||||
export const deleteGroup: (
|
||||
db: QuickSQLiteConnection,
|
||||
groupId: string,
|
||||
) => Promise<QueryResult> = async (db, groupId) => {
|
||||
const deleteMessagesQuery = `
|
||||
DELETE FROM nostros_group_messages
|
||||
WHERE group_id = ?
|
||||
`
|
||||
await db.execute(deleteMessagesQuery, [groupId])
|
||||
const deleteQuery = `
|
||||
DELETE FROM nostros_groups
|
||||
WHERE id = ?
|
||||
`
|
||||
return db.execute(deleteQuery, [groupId])
|
||||
}
|
||||
|
@ -74,7 +74,7 @@ export const validBlueBirdUrl: (url: string | undefined) => boolean = (url) => {
|
||||
|
||||
export const validNip21: (string: string | undefined) => boolean = (string) => {
|
||||
if (string) {
|
||||
const regexp = /^(nostr:)?(npub1|nprofile1|nevent1|nrelay1)\S*$/
|
||||
const regexp = /^(nostr:)?(npub1|nprofile1|nevent1|nrelay1|note1)\S*$/
|
||||
return regexp.test(string)
|
||||
} else {
|
||||
return false
|
||||
|
@ -62,6 +62,9 @@
|
||||
"cancel": "Abbrechen",
|
||||
"poweredBy": "poweredBy {{uri}}"
|
||||
},
|
||||
"groupPage": {
|
||||
"typeMessage": "Nachricht schreiben"
|
||||
},
|
||||
"menuItems": {
|
||||
"relays": "Relays",
|
||||
"contacts": "Contacts",
|
||||
|
@ -310,6 +310,22 @@
|
||||
"isFollower": "follows you",
|
||||
"relaysDescription": "These are {{username}}'s relays, activate the ones you want to be connected."
|
||||
},
|
||||
"groupPage": {
|
||||
"typeMessage": "Type message"
|
||||
},
|
||||
"groupsFeed": {
|
||||
"delete": "Delete",
|
||||
"updateTitle": "Update group",
|
||||
"newGroupName": "Name",
|
||||
"newGroupDescription": "Description",
|
||||
"newGroupPicture": "Picture",
|
||||
"groupUpdate": "Update",
|
||||
"newGroupCreate": "Create",
|
||||
"createTitle": "Create group",
|
||||
"groupId": "Group id",
|
||||
"addTitle": "Add existing group",
|
||||
"add": "Add"
|
||||
},
|
||||
"homePage": {
|
||||
"clipboardTitle": "Nostr key detected",
|
||||
"goToEvent": "Open",
|
||||
|
@ -62,6 +62,9 @@
|
||||
"cancel": "Cancelar",
|
||||
"poweredBy": "Servido por {{uri}}"
|
||||
},
|
||||
"groupPage": {
|
||||
"typeMessage": "Escribir mensaje"
|
||||
},
|
||||
"menuItems": {
|
||||
"relays": "Relays",
|
||||
"contacts": "Contactos",
|
||||
|
@ -62,6 +62,9 @@
|
||||
"cancel": "Cancel",
|
||||
"poweredBy": "Powered by {{uri}}"
|
||||
},
|
||||
"groupPage": {
|
||||
"typeMessage": "Écrire un message"
|
||||
},
|
||||
"menuItems": {
|
||||
"relays": "Relais",
|
||||
"contacts": "Contacts",
|
||||
|
@ -62,6 +62,9 @@
|
||||
"cancel": "Отменить",
|
||||
"poweredBy": "Powered by {{uri}}"
|
||||
},
|
||||
"groupPage": {
|
||||
"typeMessage": "Напишите сообщение"
|
||||
},
|
||||
"menuItems": {
|
||||
"relays": "Реле",
|
||||
"contacts": "Contacts",
|
||||
|
@ -61,6 +61,9 @@
|
||||
"cancel": "取消",
|
||||
"poweredBy": "由{{uri}}提供支持"
|
||||
},
|
||||
"groupPage": {
|
||||
"typeMessage": "输入信息"
|
||||
},
|
||||
"menuItems": {
|
||||
"contacts": "Contacts",
|
||||
"relays": "中继",
|
||||
@ -143,7 +146,7 @@
|
||||
"contactRemoved": "已取消关注",
|
||||
"contactUnblocked": "用户已屏蔽"
|
||||
},
|
||||
"emptyTitleBlocked": "您还没有屏蔽任何人",
|
||||
"emptyTitleBlocked": "您还没有屏蔽任何人",
|
||||
"emptyDescriptionBlocked": "您可以随时在用户详情页屏蔽用户",
|
||||
"emptyTitleFollowing": "您还没有关注任何用户",
|
||||
"emptyDescriptionFollowing": "关注一些用户以查看内容",
|
||||
|
@ -34,7 +34,7 @@ export const ConversationPage: React.FC<ConversationPageProps> = ({ route }) =>
|
||||
const scrollViewRef = useRef<ScrollView>()
|
||||
const { database, setRefreshBottomBarAt, setDisplayUserDrawer } = useContext(AppContext)
|
||||
const { relayPool, lastEventId } = useContext(RelayPoolContext)
|
||||
const { publicKey, privateKey, name, picture } = useContext(UserContext)
|
||||
const { publicKey, privateKey, name, picture, validNip05 } = useContext(UserContext)
|
||||
const otherPubKey = useMemo(() => route.params.pubKey, [])
|
||||
const [pageSize, setPageSize] = useState<number>(initialPageSize)
|
||||
const [decryptedMessages, setDecryptedMessages] = useState<Record<string, string>>({})
|
||||
@ -91,7 +91,8 @@ export const ConversationPage: React.FC<ConversationPageProps> = ({ route }) =>
|
||||
return message
|
||||
}),
|
||||
)
|
||||
if (subscribe) subscribeDirectMessages(results[0].created_at)
|
||||
const lastCreateAt = pageSize <= results.length ? results[0].created_at : 0
|
||||
if (subscribe) subscribeDirectMessages(lastCreateAt)
|
||||
} else if (subscribe) {
|
||||
subscribeDirectMessages()
|
||||
}
|
||||
@ -144,6 +145,7 @@ export const ConversationPage: React.FC<ConversationPageProps> = ({ route }) =>
|
||||
|
||||
const directMessage = event as DirectMessage
|
||||
directMessage.pending = true
|
||||
directMessage.valid_nip05 = validNip05
|
||||
setSendingMessages((prev) => [...prev, directMessage])
|
||||
setInput('')
|
||||
}
|
||||
@ -155,6 +157,7 @@ export const ConversationPage: React.FC<ConversationPageProps> = ({ route }) =>
|
||||
const displayName =
|
||||
item.pubkey === publicKey ? usernamePubKey(name, publicKey) : username(otherUser)
|
||||
const showAvatar = directMessages[index - 1]?.pubkey !== item.pubkey
|
||||
const nip05 = item.pubkey === publicKey ? validNip05 : otherUser.valid_nip05
|
||||
|
||||
return (
|
||||
<View style={styles.messageRow}>
|
||||
@ -184,7 +187,18 @@ export const ConversationPage: React.FC<ConversationPageProps> = ({ route }) =>
|
||||
>
|
||||
<Card.Content>
|
||||
<View style={styles.cardContentInfo}>
|
||||
<Text>{displayName}</Text>
|
||||
<View style={styles.cardContentName}>
|
||||
<Text>{displayName}</Text>
|
||||
{nip05 ? (
|
||||
<MaterialCommunityIcons
|
||||
name='check-decagram-outline'
|
||||
color={theme.colors.onPrimaryContainer}
|
||||
style={styles.verifyIcon}
|
||||
/>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</View>
|
||||
<View style={styles.cardContentDate}>
|
||||
{item.pending && (
|
||||
<View style={styles.cardContentPending}>
|
||||
@ -310,7 +324,9 @@ const styles = StyleSheet.create({
|
||||
flexDirection: 'row',
|
||||
},
|
||||
container: {
|
||||
padding: 16,
|
||||
paddingLeft: 16,
|
||||
paddingBottom: 16,
|
||||
paddingRight: 16,
|
||||
justifyContent: 'space-between',
|
||||
flex: 1,
|
||||
},
|
||||
@ -349,6 +365,13 @@ const styles = StyleSheet.create({
|
||||
margin: 16,
|
||||
bottom: 70,
|
||||
},
|
||||
verifyIcon: {
|
||||
paddingTop: 4,
|
||||
paddingLeft: 5,
|
||||
},
|
||||
cardContentName: {
|
||||
flexDirection: 'row',
|
||||
},
|
||||
})
|
||||
|
||||
export default ConversationPage
|
||||
|
@ -121,8 +121,8 @@ export const HomeNavigator: React.FC = () => {
|
||||
{['Landing'].includes(route.name) && historyKey?.includes('messages-') && (
|
||||
<Appbar.Action icon='check-all' isLeading onPress={() => onPressCheckAll()} />
|
||||
)}
|
||||
{['GroupPage'].includes(route.name) && (
|
||||
<GroupHeaderIcon groupId={route.params?.groupId}/>
|
||||
{['Group'].includes(route.name) && (
|
||||
<GroupHeaderIcon groupId={route.params?.groupId} />
|
||||
)}
|
||||
</Appbar.Header>
|
||||
)
|
||||
@ -137,7 +137,7 @@ export const HomeNavigator: React.FC = () => {
|
||||
<Stack.Screen name='Repost' component={SendPage} />
|
||||
<Stack.Screen name='Reply' component={SendPage} />
|
||||
<Stack.Screen name='Conversation' component={ConversationPage} />
|
||||
<Stack.Screen name='GroupPage' component={GroupPage} />
|
||||
<Stack.Screen name='Group' component={GroupPage} />
|
||||
</Stack.Group>
|
||||
<Stack.Group>
|
||||
<Stack.Screen name='Contacts' component={ContactsPage} />
|
||||
|
@ -3,18 +3,11 @@ import { NativeScrollEvent, NativeSyntheticEvent, ScrollView, StyleSheet, View }
|
||||
import { AppContext } from '../../Contexts/AppContext'
|
||||
import { RelayPoolContext } from '../../Contexts/RelayPoolContext'
|
||||
import { Event } from '../../lib/nostr/Events'
|
||||
import {
|
||||
DirectMessage,
|
||||
getDirectMessages,
|
||||
updateConversationRead,
|
||||
} from '../../Functions/DatabaseFunctions/DirectMessages'
|
||||
import { getUser, User } from '../../Functions/DatabaseFunctions/Users'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { username, usernamePubKey, usersToTags } from '../../Functions/RelayFunctions/Users'
|
||||
import { username, usernamePubKey } from '../../Functions/RelayFunctions/Users'
|
||||
import { getUnixTime, formatDistance, fromUnixTime } from 'date-fns'
|
||||
import TextContent from '../../Components/TextContent'
|
||||
import { encrypt, decrypt } from '../../lib/nostr/Nip04'
|
||||
import { Card, useTheme, TextInput, Snackbar, TouchableRipple, Text } from 'react-native-paper'
|
||||
import { Card, useTheme, TextInput, TouchableRipple, Text } from 'react-native-paper'
|
||||
import { UserContext } from '../../Contexts/UserContext'
|
||||
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons'
|
||||
import { useFocusEffect } from '@react-navigation/native'
|
||||
@ -23,6 +16,8 @@ import { handleInfinityScroll } from '../../Functions/NativeFunctions'
|
||||
import NostrosAvatar from '../../Components/NostrosAvatar'
|
||||
import { FlashList, ListRenderItem } from '@shopify/flash-list'
|
||||
import UploadImage from '../../Components/UploadImage'
|
||||
import { getGroupMessages, GroupMessage } from '../../Functions/DatabaseFunctions/Groups'
|
||||
import { RelayFilters } from '../../lib/nostr/RelayPool/intex'
|
||||
|
||||
interface GroupPageProps {
|
||||
route: { params: { groupId: string } }
|
||||
@ -32,16 +27,14 @@ export const GroupPage: React.FC<GroupPageProps> = ({ route }) => {
|
||||
const initialPageSize = 10
|
||||
const theme = useTheme()
|
||||
const scrollViewRef = useRef<ScrollView>()
|
||||
const { database, setRefreshBottomBarAt, setDisplayUserDrawer } = useContext(AppContext)
|
||||
const { database, setDisplayUserDrawer } = useContext(AppContext)
|
||||
const { relayPool, lastEventId } = useContext(RelayPoolContext)
|
||||
const { publicKey, privateKey, name, picture } = useContext(UserContext)
|
||||
const { publicKey, privateKey, name, picture, validNip05 } = useContext(UserContext)
|
||||
const otherPubKey = useMemo(() => route.params.groupId, [])
|
||||
const [pageSize, setPageSize] = useState<number>(initialPageSize)
|
||||
const [directMessages, setDirectMessages] = useState<DirectMessage[]>([])
|
||||
const [sendingMessages, setSendingMessages] = useState<DirectMessage[]>([])
|
||||
const [otherUser, setOtherUser] = useState<User>({ id: otherPubKey })
|
||||
const [groupMessages, setGroupMessages] = useState<GroupMessage[]>([])
|
||||
const [sendingMessages, setSendingMessages] = useState<GroupMessage[]>([])
|
||||
const [input, setInput] = useState<string>('')
|
||||
const [showNotification, setShowNotification] = useState<string>()
|
||||
const [startUpload, setStartUpload] = useState<boolean>(false)
|
||||
const [uploadingFile, setUploadingFile] = useState<boolean>(false)
|
||||
|
||||
@ -49,105 +42,96 @@ export const GroupPage: React.FC<GroupPageProps> = ({ route }) => {
|
||||
|
||||
useFocusEffect(
|
||||
React.useCallback(() => {
|
||||
loadDirectMessages(true)
|
||||
subscribeDirectMessages()
|
||||
loadGroupMessages(true)
|
||||
|
||||
return () => relayPool?.unsubscribe([`conversation${route.params.groupId}`])
|
||||
return () => relayPool?.unsubscribe([`group${route.params.groupId}`])
|
||||
}, []),
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
loadDirectMessages(false)
|
||||
loadGroupMessages(false)
|
||||
}, [lastEventId])
|
||||
|
||||
const loadDirectMessages: (subscribe: boolean) => void = (subscribe) => {
|
||||
if (database && publicKey && privateKey) {
|
||||
const conversationId = route.params?.conversationId
|
||||
updateConversationRead(conversationId, database)
|
||||
setRefreshBottomBarAt(getUnixTime(new Date()))
|
||||
getUser(otherPubKey, database).then((user) => {
|
||||
if (user) setOtherUser(user)
|
||||
})
|
||||
getDirectMessages(database, conversationId, publicKey, otherPubKey, {
|
||||
const loadGroupMessages: (subscribe: boolean) => void = (subscribe) => {
|
||||
if (database && publicKey && privateKey && route.params.groupId) {
|
||||
getGroupMessages(database, route.params.groupId, {
|
||||
order: 'DESC',
|
||||
limit: pageSize,
|
||||
}).then((results) => {
|
||||
if (results.length > 0) {
|
||||
setSendingMessages([])
|
||||
setDirectMessages((prev) => {
|
||||
return results.map((message, index) => {
|
||||
if (prev.length > index) {
|
||||
return prev[index]
|
||||
} else {
|
||||
message.content = decrypt(privateKey, otherPubKey, message.content ?? '')
|
||||
return message
|
||||
}
|
||||
})
|
||||
})
|
||||
if (subscribe) subscribeDirectMessages(results[0].created_at)
|
||||
setGroupMessages(results)
|
||||
const pubKeys = results
|
||||
.map((message) => message.pubkey)
|
||||
.filter((key, index, array) => array.indexOf(key) === index)
|
||||
const lastCreateAt = pageSize <= results.length ? results[0].created_at : 0
|
||||
if (subscribe) subscribeGroupMessages(lastCreateAt, pubKeys)
|
||||
} else if (subscribe) {
|
||||
subscribeDirectMessages()
|
||||
subscribeGroupMessages()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const subscribeDirectMessages: (lastCreateAt?: number) => void = async (lastCreateAt) => {
|
||||
if (publicKey && otherPubKey) {
|
||||
relayPool?.subscribe(`conversation${route.params.groupId}`, [
|
||||
const subscribeGroupMessages: (lastCreateAt?: number, pubKeys?: string[]) => void = async (
|
||||
lastCreateAt,
|
||||
pubKeys,
|
||||
) => {
|
||||
if (publicKey && otherPubKey && route.params.groupId) {
|
||||
const filters: RelayFilters[] = [
|
||||
{
|
||||
kinds: [Kind.EncryptedDirectMessage],
|
||||
authors: [publicKey],
|
||||
'#p': [otherPubKey],
|
||||
since: lastCreateAt ?? 0,
|
||||
kinds: [Kind.ChannelCreation],
|
||||
ids: [route.params.groupId],
|
||||
},
|
||||
{
|
||||
kinds: [Kind.EncryptedDirectMessage],
|
||||
authors: [otherPubKey],
|
||||
'#p': [publicKey],
|
||||
since: lastCreateAt ?? 0,
|
||||
kinds: [Kind.ChannelMetadata],
|
||||
'#e': [route.params.groupId],
|
||||
},
|
||||
])
|
||||
{
|
||||
kinds: [Kind.ChannelMessage],
|
||||
'#e': [route.params.groupId],
|
||||
since: lastCreateAt ?? 0,
|
||||
limit: pageSize,
|
||||
},
|
||||
]
|
||||
|
||||
if (pubKeys && pubKeys.length > 0) {
|
||||
filters.push({
|
||||
kinds: [Kind.Metadata],
|
||||
authors: pubKeys,
|
||||
})
|
||||
}
|
||||
|
||||
relayPool?.subscribe(`group${route.params.groupId}`, filters)
|
||||
}
|
||||
}
|
||||
|
||||
const send: () => void = () => {
|
||||
if (input !== '' && otherPubKey && publicKey && privateKey) {
|
||||
if (input !== '' && otherPubKey && publicKey && privateKey && route.params.groupId) {
|
||||
const event: Event = {
|
||||
content: input,
|
||||
created_at: getUnixTime(new Date()),
|
||||
kind: Kind.EncryptedDirectMessage,
|
||||
kind: Kind.ChannelMessage,
|
||||
pubkey: publicKey,
|
||||
tags: usersToTags([otherUser]),
|
||||
tags: [['e', route.params.groupId, '']],
|
||||
}
|
||||
encrypt(privateKey, otherPubKey, input)
|
||||
.then((encryptedcontent) => {
|
||||
relayPool
|
||||
?.sendEvent({
|
||||
...event,
|
||||
content: encryptedcontent,
|
||||
})
|
||||
.catch(() => {
|
||||
setShowNotification('privateMessageSendError')
|
||||
})
|
||||
})
|
||||
.catch(() => {
|
||||
setShowNotification('privateMessageSendError')
|
||||
})
|
||||
|
||||
const directMessage = event as DirectMessage
|
||||
directMessage.pending = true
|
||||
setSendingMessages((prev) => [...prev, directMessage])
|
||||
relayPool?.sendEvent(event)
|
||||
const groupMessage = event as GroupMessage
|
||||
groupMessage.pending = true
|
||||
groupMessage.valid_nip05 = validNip05
|
||||
setSendingMessages((prev) => [...prev, groupMessage])
|
||||
setInput('')
|
||||
}
|
||||
}
|
||||
|
||||
const renderDirectMessageItem: ListRenderItem<DirectMessage> = ({ index, item }) => {
|
||||
if (!publicKey || !privateKey || !otherUser) return <></>
|
||||
const renderGroupMessageItem: ListRenderItem<GroupMessage> = ({ index, item }) => {
|
||||
if (!publicKey) return <></>
|
||||
|
||||
const displayName =
|
||||
item.pubkey === publicKey ? usernamePubKey(name, publicKey) : username(otherUser)
|
||||
const showAvatar = directMessages[index - 1]?.pubkey !== item.pubkey
|
||||
item.pubkey === publicKey
|
||||
? usernamePubKey(name, publicKey)
|
||||
: username({ name: item.name, id: item.pubkey })
|
||||
const showAvatar = groupMessages[index - 1]?.pubkey !== item.pubkey
|
||||
|
||||
return (
|
||||
<View style={styles.messageRow}>
|
||||
@ -156,9 +140,9 @@ export const GroupPage: React.FC<GroupPageProps> = ({ route }) => {
|
||||
{showAvatar && (
|
||||
<TouchableRipple onPress={() => setDisplayUserDrawer(otherPubKey)}>
|
||||
<NostrosAvatar
|
||||
name={otherUser.name}
|
||||
name={displayName}
|
||||
pubKey={otherPubKey}
|
||||
src={otherUser.picture}
|
||||
src={item.picture}
|
||||
size={40}
|
||||
/>
|
||||
</TouchableRipple>
|
||||
@ -177,7 +161,18 @@ export const GroupPage: React.FC<GroupPageProps> = ({ route }) => {
|
||||
>
|
||||
<Card.Content>
|
||||
<View style={styles.cardContentInfo}>
|
||||
<Text>{displayName}</Text>
|
||||
<View style={styles.cardContentName}>
|
||||
<Text>{displayName}</Text>
|
||||
{item.valid_nip05 ? (
|
||||
<MaterialCommunityIcons
|
||||
name='check-decagram-outline'
|
||||
color={theme.colors.onPrimaryContainer}
|
||||
style={styles.verifyIcon}
|
||||
/>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</View>
|
||||
<View style={styles.cardContentDate}>
|
||||
{item.pending && (
|
||||
<View style={styles.cardContentPending}>
|
||||
@ -193,7 +188,7 @@ export const GroupPage: React.FC<GroupPageProps> = ({ route }) => {
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
<TextContent content={item.content} />
|
||||
<TextContent content={item.content} event={item} />
|
||||
</Card.Content>
|
||||
</Card>
|
||||
{publicKey === item.pubkey && (
|
||||
@ -219,8 +214,8 @@ export const GroupPage: React.FC<GroupPageProps> = ({ route }) => {
|
||||
<View style={styles.container}>
|
||||
<FlashList
|
||||
inverted
|
||||
data={[...sendingMessages, ...directMessages]}
|
||||
renderItem={renderDirectMessageItem}
|
||||
data={[...sendingMessages, ...groupMessages]}
|
||||
renderItem={renderGroupMessageItem}
|
||||
horizontal={false}
|
||||
ref={scrollViewRef}
|
||||
estimatedItemSize={100}
|
||||
@ -237,7 +232,7 @@ export const GroupPage: React.FC<GroupPageProps> = ({ route }) => {
|
||||
<TextInput
|
||||
mode='outlined'
|
||||
multiline
|
||||
label={t('conversationPage.typeMessage') ?? ''}
|
||||
label={t('groupPage.typeMessage') ?? ''}
|
||||
value={input}
|
||||
onChangeText={setInput}
|
||||
onFocus={() => scrollViewRef.current?.scrollToEnd({ animated: true })}
|
||||
@ -267,17 +262,6 @@ export const GroupPage: React.FC<GroupPageProps> = ({ route }) => {
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
{showNotification && (
|
||||
<Snackbar
|
||||
style={styles.snackbar}
|
||||
visible={showNotification !== undefined}
|
||||
duration={Snackbar.DURATION_SHORT}
|
||||
onIconPress={() => setShowNotification(undefined)}
|
||||
onDismiss={() => setShowNotification(undefined)}
|
||||
>
|
||||
{t(`conversationPage.notifications.${showNotification}`)}
|
||||
</Snackbar>
|
||||
)}
|
||||
<UploadImage
|
||||
startUpload={startUpload}
|
||||
setImageUri={(imageUri) => {
|
||||
@ -303,7 +287,9 @@ const styles = StyleSheet.create({
|
||||
flexDirection: 'row',
|
||||
},
|
||||
container: {
|
||||
padding: 16,
|
||||
paddingLeft: 16,
|
||||
paddingBottom: 16,
|
||||
paddingRight: 16,
|
||||
justifyContent: 'space-between',
|
||||
flex: 1,
|
||||
},
|
||||
@ -342,6 +328,13 @@ const styles = StyleSheet.create({
|
||||
margin: 16,
|
||||
bottom: 70,
|
||||
},
|
||||
verifyIcon: {
|
||||
paddingTop: 4,
|
||||
paddingLeft: 5,
|
||||
},
|
||||
cardContentName: {
|
||||
flexDirection: 'row',
|
||||
},
|
||||
})
|
||||
|
||||
export default GroupPage
|
||||
|
@ -1,10 +1,12 @@
|
||||
import React, { useContext, useEffect, useState } from 'react'
|
||||
import { Dimensions, StyleSheet, View } from 'react-native'
|
||||
import { Clipboard, Dimensions, FlatList, StyleSheet, View } from 'react-native'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
AnimatedFAB,
|
||||
Button,
|
||||
Divider,
|
||||
List,
|
||||
Snackbar,
|
||||
Text,
|
||||
TextInput,
|
||||
TouchableRipple,
|
||||
@ -25,25 +27,31 @@ import { formatId, username } from '../../Functions/RelayFunctions/Users'
|
||||
import NostrosAvatar from '../../Components/NostrosAvatar'
|
||||
import { navigate } from '../../lib/Navigation'
|
||||
import { FlashList, ListRenderItem } from '@shopify/flash-list'
|
||||
import { RelayFilters } from '../../lib/nostr/RelayPool/intex'
|
||||
import { validNip21 } from '../../Functions/NativeFunctions'
|
||||
import { getNip19Key } from '../../lib/nostr/Nip19'
|
||||
|
||||
export const GroupsFeed: React.FC = () => {
|
||||
const { t } = useTranslation('common')
|
||||
const theme = useTheme()
|
||||
const { database } = useContext(AppContext)
|
||||
const { publicKey } = useContext(UserContext)
|
||||
const { relayPool, lastEventId } = useContext(RelayPoolContext)
|
||||
const { relayPool, lastEventId, lastConfirmationtId } = useContext(RelayPoolContext)
|
||||
const bottomSheetSearchRef = React.useRef<RBSheet>(null)
|
||||
const bottomSheetCreateRef = React.useRef<RBSheet>(null)
|
||||
const bottomSheetFabActionRef = React.useRef<RBSheet>(null)
|
||||
const [groups, setGroups] = useState<Group[]>([])
|
||||
const [searchGroup, setSearchGroup] = useState<string>()
|
||||
const [newGroupName, setNewGroupName] = useState<string>()
|
||||
const [newGroupDescription, setNewGroupDescription] = useState<string>()
|
||||
const [newGroupPicture, setNewGroupPicture] = useState<string>()
|
||||
const [startUpload, setStartUpload] = useState<boolean>(false)
|
||||
const [uploadingFile, setUploadingFile] = useState<boolean>(false)
|
||||
const [showNotification, setShowNotification] = useState<string>()
|
||||
|
||||
useFocusEffect(
|
||||
React.useCallback(() => {
|
||||
loadGroups()
|
||||
subscribeGroups()
|
||||
|
||||
return () => relayPool?.unsubscribe(['groups-create', 'groups-meta', 'groups-messages'])
|
||||
}, []),
|
||||
@ -51,27 +59,56 @@ export const GroupsFeed: React.FC = () => {
|
||||
|
||||
useEffect(() => {
|
||||
loadGroups()
|
||||
}, [lastEventId])
|
||||
}, [lastEventId, lastConfirmationtId])
|
||||
|
||||
const subscribeGroups: () => void = async () => {
|
||||
if (publicKey) {
|
||||
relayPool?.subscribe('groups-create', [
|
||||
{
|
||||
kinds: [Kind.ChannelCreation],
|
||||
authors: [publicKey],
|
||||
},
|
||||
])
|
||||
const pastePicture: () => void = () => {
|
||||
Clipboard.getString().then((value) => {
|
||||
setNewGroupPicture(value ?? '')
|
||||
})
|
||||
}
|
||||
|
||||
const loadGroups: (newGroupId?: string) => void = (newGroupId) => {
|
||||
if (database && publicKey) {
|
||||
getGroups(database).then((results) => {
|
||||
const filters: RelayFilters[] = [
|
||||
{
|
||||
kinds: [Kind.ChannelCreation],
|
||||
authors: [publicKey],
|
||||
},
|
||||
]
|
||||
if (results && results.length > 0) {
|
||||
setGroups(results)
|
||||
filters.push({
|
||||
kinds: [Kind.Metadata],
|
||||
ids: [...results.map((group) => group.pubkey)],
|
||||
})
|
||||
filters.push({
|
||||
kinds: [Kind.ChannelMetadata],
|
||||
ids: [...results.map((group) => group.id ?? ''), publicKey, newGroupId ?? ''],
|
||||
})
|
||||
if (newGroupId) {
|
||||
filters.push({
|
||||
kinds: [Kind.ChannelCreation],
|
||||
ids: [newGroupId],
|
||||
})
|
||||
}
|
||||
}
|
||||
relayPool?.subscribe('groups-create', filters)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const loadGroups: () => void = () => {
|
||||
if (database && publicKey) {
|
||||
getGroups(database).then((results) => {
|
||||
if (results && results.length > 0) {
|
||||
setGroups(results)
|
||||
}
|
||||
})
|
||||
const addGroup: () => void = () => {
|
||||
if (!searchGroup) return
|
||||
if (validNip21(searchGroup)) {
|
||||
const key = getNip19Key(searchGroup)
|
||||
if (key) loadGroups(key)
|
||||
} else {
|
||||
loadGroups(searchGroup)
|
||||
}
|
||||
setSearchGroup(undefined)
|
||||
bottomSheetSearchRef.current?.close()
|
||||
bottomSheetFabActionRef.current?.close()
|
||||
}
|
||||
|
||||
const createNewGroup: () => void = () => {
|
||||
@ -89,6 +126,7 @@ export const GroupsFeed: React.FC = () => {
|
||||
}
|
||||
relayPool?.sendEvent(event)
|
||||
bottomSheetCreateRef.current?.close()
|
||||
bottomSheetFabActionRef.current?.close()
|
||||
}
|
||||
}
|
||||
|
||||
@ -96,7 +134,7 @@ export const GroupsFeed: React.FC = () => {
|
||||
return (
|
||||
<TouchableRipple
|
||||
onPress={() =>
|
||||
navigate('GroupPage', {
|
||||
navigate('Group', {
|
||||
groupId: item.id,
|
||||
title: item.name || formatId(item.id),
|
||||
})
|
||||
@ -158,14 +196,53 @@ export const GroupsFeed: React.FC = () => {
|
||||
color={theme.colors.onPrimaryContainer}
|
||||
/>
|
||||
<Text variant='headlineSmall' style={styles.center}>
|
||||
{t('contactsPage.emptyTitleBlocked')}
|
||||
{t('groupsFeed.emptyTitleBlocked')}
|
||||
</Text>
|
||||
<Text variant='bodyMedium' style={styles.center}>
|
||||
{t('contactsPage.emptyDescriptionBlocked')}
|
||||
{t('groupsFeed.emptyDescriptionBlocked')}
|
||||
</Text>
|
||||
</View>
|
||||
)
|
||||
|
||||
const fabOptions = React.useMemo(() => {
|
||||
return [
|
||||
{
|
||||
key: 1,
|
||||
title: t('groupsFeed.createTitle'),
|
||||
left: () => (
|
||||
<List.Icon
|
||||
icon={() => (
|
||||
<MaterialCommunityIcons
|
||||
name='account-multiple-plus-outline'
|
||||
size={25}
|
||||
color={theme.colors.onPrimaryContainer}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
),
|
||||
onPress: async () => bottomSheetCreateRef.current?.open(),
|
||||
},
|
||||
{
|
||||
key: 2,
|
||||
title: t('groupsFeed.add'),
|
||||
left: () => (
|
||||
<List.Icon
|
||||
icon={() => (
|
||||
<MaterialCommunityIcons
|
||||
name='plus'
|
||||
size={25}
|
||||
color={theme.colors.onPrimaryContainer}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
),
|
||||
onPress: async () => bottomSheetSearchRef.current?.open(),
|
||||
disabled: false,
|
||||
style: {},
|
||||
},
|
||||
]
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<FlashList
|
||||
@ -181,7 +258,7 @@ export const GroupsFeed: React.FC = () => {
|
||||
style={[styles.fab, { top: Dimensions.get('window').height - 216 }]}
|
||||
icon='plus'
|
||||
label='Label'
|
||||
onPress={() => bottomSheetCreateRef.current?.open()}
|
||||
onPress={() => bottomSheetFabActionRef.current?.open()}
|
||||
animateFrom='right'
|
||||
iconMode='static'
|
||||
extended={false}
|
||||
@ -208,7 +285,6 @@ export const GroupsFeed: React.FC = () => {
|
||||
/>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
multiline
|
||||
mode='outlined'
|
||||
label={t('groupsFeed.newGroupPicture') ?? ''}
|
||||
onChangeText={setNewGroupPicture}
|
||||
@ -225,6 +301,13 @@ export const GroupsFeed: React.FC = () => {
|
||||
onPress={() => setStartUpload(true)}
|
||||
/>
|
||||
}
|
||||
right={
|
||||
<TextInput.Icon
|
||||
icon='content-paste'
|
||||
onPress={pastePicture}
|
||||
forceTextInputFocus={false}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Button mode='contained' disabled={!newGroupName} onPress={createNewGroup}>
|
||||
{t('groupsFeed.newGroupCreate')}
|
||||
@ -240,6 +323,57 @@ export const GroupsFeed: React.FC = () => {
|
||||
/>
|
||||
</View>
|
||||
</RBSheet>
|
||||
<RBSheet ref={bottomSheetSearchRef} closeOnDragDown={true} customStyles={bottomSheetStyles}>
|
||||
<View>
|
||||
<Text style={styles.input} variant='titleLarge'>
|
||||
{t('groupsFeed.addTitle')}
|
||||
</Text>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
mode='outlined'
|
||||
label={t('groupsFeed.groupId') ?? ''}
|
||||
onChangeText={setSearchGroup}
|
||||
value={searchGroup}
|
||||
/>
|
||||
<Button mode='contained' disabled={!searchGroup} onPress={addGroup}>
|
||||
{t('groupsFeed.add')}
|
||||
</Button>
|
||||
</View>
|
||||
</RBSheet>
|
||||
<RBSheet
|
||||
ref={bottomSheetFabActionRef}
|
||||
closeOnDragDown={true}
|
||||
customStyles={bottomSheetStyles}
|
||||
>
|
||||
<FlatList
|
||||
data={fabOptions}
|
||||
renderItem={({ item }) => {
|
||||
return (
|
||||
<List.Item
|
||||
key={item.key}
|
||||
title={item.title}
|
||||
onPress={item.onPress}
|
||||
left={item.left}
|
||||
disabled={item.disabled}
|
||||
titleStyle={item.style}
|
||||
/>
|
||||
)
|
||||
}}
|
||||
ItemSeparatorComponent={Divider}
|
||||
horizontal={false}
|
||||
/>
|
||||
</RBSheet>
|
||||
{showNotification && (
|
||||
<Snackbar
|
||||
style={styles.snackbar}
|
||||
visible={showNotification !== undefined}
|
||||
duration={Snackbar.DURATION_SHORT}
|
||||
onIconPress={() => setShowNotification(undefined)}
|
||||
onDismiss={() => setShowNotification(undefined)}
|
||||
>
|
||||
{t(`groupsFeed.notifications.${showNotification}`)}
|
||||
</Snackbar>
|
||||
)}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@ -272,6 +406,10 @@ const styles = StyleSheet.create({
|
||||
paddingLeft: 16,
|
||||
paddingRight: 16,
|
||||
},
|
||||
snackbar: {
|
||||
marginLeft: 16,
|
||||
bottom: 16,
|
||||
},
|
||||
containerAvatar: {
|
||||
marginTop: 10,
|
||||
},
|
||||
|
@ -231,7 +231,7 @@ const styles = StyleSheet.create({
|
||||
profileData: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
paddingBottom: 16
|
||||
paddingBottom: 16,
|
||||
},
|
||||
})
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user