mirror of
https://github.com/KoalaSat/nostros.git
synced 2024-09-28 22:30:41 +00:00
Groups list (#308)
This commit is contained in:
commit
187e13808a
@ -53,7 +53,7 @@ public class Event {
|
||||
if (kind.equals("0")) {
|
||||
saveUserMeta(database);
|
||||
} else if (kind.equals("1") || kind.equals("2")) {
|
||||
saveNote(database, userPubKey, relayUrl);
|
||||
saveNote(database, userPubKey);
|
||||
} else if (kind.equals("3")) {
|
||||
if (pubkey.equals(userPubKey)) {
|
||||
savePets(database);
|
||||
@ -64,6 +64,8 @@ public class Event {
|
||||
saveDirectMessage(database);
|
||||
} else if (kind.equals("7")) {
|
||||
saveReaction(database);
|
||||
} else if (kind.equals("40")) {
|
||||
saveGroup(database);
|
||||
}
|
||||
} catch (JSONException e) {
|
||||
e.printStackTrace();
|
||||
@ -185,7 +187,7 @@ public class Event {
|
||||
return false;
|
||||
}
|
||||
|
||||
protected void saveNote(SQLiteDatabase database, String userPubKey, String relayUrl) {
|
||||
protected void saveNote(SQLiteDatabase database, String userPubKey) {
|
||||
ContentValues values = new ContentValues();
|
||||
values.put("id", id);
|
||||
values.put("content", content);
|
||||
@ -201,6 +203,23 @@ public class Event {
|
||||
database.replace("nostros_notes", null, values);
|
||||
}
|
||||
|
||||
protected void saveGroup(SQLiteDatabase database) throws JSONException {
|
||||
JSONObject groupContent = new JSONObject(content);
|
||||
|
||||
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("name", groupContent.optString("name"));
|
||||
values.put("about", groupContent.optString("about"));
|
||||
values.put("picture", groupContent.optString("picture"));
|
||||
database.replace("nostros_groups", null, values);
|
||||
}
|
||||
|
||||
protected void saveDirectMessage(SQLiteDatabase database) throws JSONException {
|
||||
JSONArray tag = tags.getJSONArray(0);
|
||||
ArrayList<String> identifiers = new ArrayList<>();
|
||||
|
@ -141,6 +141,21 @@ public class DatabaseModule {
|
||||
database.execSQL("ALTER TABLE nostros_relays ADD COLUMN resilient INT DEFAULT 0;");
|
||||
database.execSQL("ALTER TABLE nostros_relays ADD COLUMN manual INT DEFAULT 1;");
|
||||
} catch (SQLException e) { }
|
||||
try {
|
||||
database.execSQL("CREATE TABLE IF NOT EXISTS nostros_groups(\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" +
|
||||
" name TEXT NOT NULL,\n" +
|
||||
" about TEXT NOT NULL,\n" +
|
||||
" picture TEXT NOT NULL\n" +
|
||||
" );");
|
||||
database.execSQL("CREATE INDEX nostros_groups_pubkey_index ON nostros_groups(pubkey);");
|
||||
} catch (SQLException e) { }
|
||||
}
|
||||
|
||||
public void saveEvent(JSONObject data, String userPubKey, String relayUrl) throws JSONException {
|
||||
|
75
frontend/Components/GroupHeaderIcon/index.tsx
Normal file
75
frontend/Components/GroupHeaderIcon/index.tsx
Normal file
@ -0,0 +1,75 @@
|
||||
import React, { useContext, useEffect, useState } from 'react'
|
||||
import { 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 { AppContext } from '../../Contexts/AppContext'
|
||||
import { validImageUrl } from '../../Functions/NativeFunctions'
|
||||
import FastImage from 'react-native-fast-image'
|
||||
|
||||
interface GroupHeaderIconProps {
|
||||
groupId: string
|
||||
}
|
||||
|
||||
export const GroupHeaderIcon: React.FC<GroupHeaderIconProps> = ({ groupId }) => {
|
||||
const { database } = useContext(AppContext)
|
||||
const theme = useTheme()
|
||||
const [group, setGroup] = useState<Group>()
|
||||
const bottomSheetEditGroupRef = React.useRef<RBSheet>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (database && groupId) {
|
||||
getGroup(database, groupId).then(setGroup)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const bottomSheetStyles = React.useMemo(() => {
|
||||
return {
|
||||
container: {
|
||||
backgroundColor: theme.colors.background,
|
||||
paddingTop: 16,
|
||||
paddingRight: 16,
|
||||
paddingBottom: 32,
|
||||
paddingLeft: 16,
|
||||
borderTopRightRadius: 28,
|
||||
borderTopLeftRadius: 28,
|
||||
height: 'auto',
|
||||
},
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<TouchableRipple onPress={() => {}}>
|
||||
{validImageUrl(group?.picture) ? (
|
||||
<FastImage
|
||||
style={[
|
||||
{
|
||||
backgroundColor: theme.colors.backdrop,
|
||||
borderRadius: 33,
|
||||
width: 35,
|
||||
height: 35,
|
||||
},
|
||||
]}
|
||||
source={{
|
||||
uri: group?.picture,
|
||||
priority: FastImage.priority.normal,
|
||||
}}
|
||||
resizeMode={FastImage.resizeMode.contain}
|
||||
/>
|
||||
) : (
|
||||
<PaperAvatar.Text size={35} label={group?.name ?? group?.id ?? ''} />
|
||||
)}
|
||||
</TouchableRipple>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
paddingRight: 8,
|
||||
},
|
||||
})
|
||||
|
||||
export default GroupHeaderIcon
|
||||
|
@ -41,6 +41,8 @@ export const MenuItems: React.FC = () => {
|
||||
navigate('About')
|
||||
} else if (key === 'config') {
|
||||
navigate('Config')
|
||||
} else if (key === 'contacts') {
|
||||
navigate('Contacts')
|
||||
}
|
||||
}
|
||||
|
||||
@ -92,8 +94,8 @@ export const MenuItems: React.FC = () => {
|
||||
</Card.Content>
|
||||
</Card>
|
||||
)}
|
||||
<Drawer.Section>
|
||||
{publicKey && (
|
||||
<Drawer.Section showDivider={false}>
|
||||
<Drawer.Item
|
||||
label={t('menuItems.relays')}
|
||||
icon={() => (
|
||||
@ -119,9 +121,15 @@ export const MenuItems: React.FC = () => {
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Drawer.Section>
|
||||
)}
|
||||
<Drawer.Section>
|
||||
<Drawer.Item
|
||||
label={t('menuItems.contacts')}
|
||||
icon='contacts-outline'
|
||||
key='contacts'
|
||||
active={drawerItemIndex === 1}
|
||||
onPress={() => onPressItem('contacts', 1)}
|
||||
onTouchEnd={() => setDrawerItemIndex(-1)}
|
||||
/>
|
||||
<Drawer.Item
|
||||
label={t('menuItems.configuration')}
|
||||
icon='cog'
|
||||
|
@ -48,11 +48,12 @@ export const NostrosAvatar: React.FC<NostrosAvatarProps> = ({
|
||||
<PaperAvatar.Text size={size} label={displayName.substring(0, 2).toUpperCase()} />
|
||||
)}
|
||||
</View>
|
||||
{hasLud06 ? (
|
||||
{hasLud06 && (
|
||||
<PaperAvatar.Icon
|
||||
size={lud06IconSize}
|
||||
icon='lightning-bolt'
|
||||
style={[
|
||||
styles.iconLightning,
|
||||
{
|
||||
right: -(size - lud06IconSize),
|
||||
backgroundColor: theme.colors.secondaryContainer,
|
||||
@ -61,8 +62,6 @@ export const NostrosAvatar: React.FC<NostrosAvatarProps> = ({
|
||||
]}
|
||||
color='#F5D112'
|
||||
/>
|
||||
) : (
|
||||
<View style={styles.iconLightning} />
|
||||
)}
|
||||
</View>
|
||||
)
|
||||
@ -70,7 +69,7 @@ export const NostrosAvatar: React.FC<NostrosAvatarProps> = ({
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
iconLightning: {
|
||||
marginBottom: 16,
|
||||
marginBottom: -16,
|
||||
},
|
||||
})
|
||||
|
||||
|
@ -473,6 +473,7 @@ const styles = StyleSheet.create({
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignContent: 'center',
|
||||
paddingBottom: 16
|
||||
},
|
||||
userBlockedWrapper: {
|
||||
flexDirection: 'row',
|
||||
|
@ -25,9 +25,14 @@ import ProfileShare from '../ProfileShare'
|
||||
interface ProfileActionsProps {
|
||||
user: User
|
||||
setUser: (user: User) => void
|
||||
onActionDone?: () => void
|
||||
}
|
||||
|
||||
export const ProfileActions: React.FC<ProfileActionsProps> = ({ user, setUser }) => {
|
||||
export const ProfileActions: React.FC<ProfileActionsProps> = ({
|
||||
user,
|
||||
setUser,
|
||||
onActionDone = () => {},
|
||||
}) => {
|
||||
const theme = useTheme()
|
||||
const { database } = React.useContext(AppContext)
|
||||
const { publicKey } = React.useContext(UserContext)
|
||||
@ -174,6 +179,7 @@ export const ProfileActions: React.FC<ProfileActionsProps> = ({ user, setUser })
|
||||
icon='message-plus-outline'
|
||||
size={28}
|
||||
onPress={() => {
|
||||
onActionDone()
|
||||
navigate('Conversation', {
|
||||
pubKey: user.id,
|
||||
title: username(user),
|
||||
|
@ -79,7 +79,7 @@ export const ProfileCard: React.FC<ProfileCardProps> = ({ bottomSheetRef, showIm
|
||||
<Divider />
|
||||
{user && (
|
||||
<View style={styles.profileActions}>
|
||||
<ProfileActions user={user} setUser={setUser} />
|
||||
<ProfileActions user={user} setUser={setUser} onActionDone={() => bottomSheetRef.current?.close()}/>
|
||||
</View>
|
||||
)}
|
||||
{showNotification && (
|
||||
@ -136,7 +136,7 @@ const styles = StyleSheet.create({
|
||||
cardUser: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
paddingTop: 16,
|
||||
padding: 16,
|
||||
},
|
||||
card: {
|
||||
flexDirection: 'row',
|
||||
@ -176,8 +176,7 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
arrow: {
|
||||
alignContent: 'center',
|
||||
justifyContent: 'center',
|
||||
marginTop: -16,
|
||||
justifyContent: 'center'
|
||||
},
|
||||
})
|
||||
|
||||
|
68
frontend/Functions/DatabaseFunctions/Groups/index.ts
Normal file
68
frontend/Functions/DatabaseFunctions/Groups/index.ts
Normal file
@ -0,0 +1,68 @@
|
||||
import { QueryResult, QuickSQLiteConnection } from 'react-native-quick-sqlite'
|
||||
import { getItems } from '..'
|
||||
import { Event } from '../../../lib/nostr/Events'
|
||||
|
||||
export interface Group extends Event {
|
||||
name: string
|
||||
about?: string
|
||||
picture?: string
|
||||
user_name?: string
|
||||
valid_nip05: boolean
|
||||
}
|
||||
|
||||
const databaseToEntity: (object: any) => Group = (object = {}) => {
|
||||
object.tags = object.tags ? JSON.parse(object.tags) : []
|
||||
return object as Group
|
||||
}
|
||||
|
||||
export const updateConversationRead: (
|
||||
conversationId: string,
|
||||
db: QuickSQLiteConnection,
|
||||
) => Promise<QueryResult | null> = async (conversationId, db) => {
|
||||
const userQuery = `UPDATE nostros_direct_messages SET read = ? WHERE conversation_id = ?`
|
||||
return db.execute(userQuery, [1, conversationId])
|
||||
}
|
||||
|
||||
export const updateAllRead: (db: QuickSQLiteConnection) => Promise<QueryResult | null> = async (
|
||||
db,
|
||||
) => {
|
||||
const userQuery = `UPDATE nostros_direct_messages SET read = ?`
|
||||
return db.execute(userQuery, [1])
|
||||
}
|
||||
|
||||
export const getGroups: (
|
||||
db: QuickSQLiteConnection
|
||||
) => Promise<Group[]> = async (db) => {
|
||||
const groupsQuery = `
|
||||
SELECT
|
||||
nostros_groups.*, nostros_users.name as user_name, nostros_users.valid_nip05
|
||||
FROM
|
||||
nostros_groups
|
||||
LEFT JOIN
|
||||
nostros_users ON nostros_users.id = nostros_groups.pubkey
|
||||
`
|
||||
const resultSet = await db.execute(groupsQuery)
|
||||
const items: object[] = getItems(resultSet)
|
||||
const notes: Group[] = items.map((object) => databaseToEntity(object))
|
||||
|
||||
return notes
|
||||
}
|
||||
|
||||
export const getGroup: (
|
||||
db: QuickSQLiteConnection,
|
||||
groupId: string
|
||||
) => Promise<Group> = async (db, groupId) => {
|
||||
const groupsQuery = `
|
||||
SELECT
|
||||
*
|
||||
FROM
|
||||
nostros_groups
|
||||
WHERE
|
||||
id = ?
|
||||
`
|
||||
const resultSet = await db.execute(groupsQuery, [groupId])
|
||||
const items: object[] = getItems(resultSet)
|
||||
const group: Group = databaseToEntity(items[0])
|
||||
|
||||
return group
|
||||
}
|
@ -17,6 +17,7 @@
|
||||
"ProfileConnect": "Profil verbinden",
|
||||
"ProfileLoad": "Profil laden",
|
||||
"Landing": "Start",
|
||||
"Contacts": "Contacts",
|
||||
"Conversation": "Unterhaltung",
|
||||
"Reply": "Antworten",
|
||||
"Repost": "Notiz zitieren",
|
||||
@ -63,6 +64,7 @@
|
||||
},
|
||||
"menuItems": {
|
||||
"relays": "Relays",
|
||||
"contacts": "Contacts",
|
||||
"notConnected": "Nicht verbunden",
|
||||
"connectedRelays": "{{number}} verbunden",
|
||||
"following": "{{following}} folge ich",
|
||||
@ -135,7 +137,7 @@
|
||||
"loginMethod": "Einloggen mit",
|
||||
"mnemonic": "Mnemonic"
|
||||
},
|
||||
"contactsFeed": {
|
||||
"contactsPage": {
|
||||
"notifications": {
|
||||
"keyCopied": "Öffentlichen Schlüssel kopiert",
|
||||
"contactAdded": "Kontakt hinzugefügt",
|
||||
|
@ -17,6 +17,7 @@
|
||||
"ProfileConnect": "",
|
||||
"ProfileLoad": "",
|
||||
"Landing": "",
|
||||
"Contacts": "Contacts",
|
||||
"Conversation": "Conversation",
|
||||
"Repost": "Repost note",
|
||||
"Reply": "Reply",
|
||||
@ -69,6 +70,7 @@
|
||||
"followers": "{{followers}} followers",
|
||||
"configuration": "Configuration",
|
||||
"about": "About",
|
||||
"contacts": "Contacts",
|
||||
"reportBug": "Report a bug",
|
||||
"logout": "Logout"
|
||||
},
|
||||
@ -135,7 +137,7 @@
|
||||
"mnemonic": "Mnemonic",
|
||||
"mnemonicInput": "Type your mnemonic words in the correct order to login."
|
||||
},
|
||||
"contactsFeed": {
|
||||
"contactsPage": {
|
||||
"notifications": {
|
||||
"keyCopied": "Public key copied.",
|
||||
"contactAdded": "Profile followed.",
|
||||
|
@ -17,6 +17,7 @@
|
||||
"ProfileConnect": "",
|
||||
"ProfileLoad": "",
|
||||
"Landing": "",
|
||||
"Contacts": "Contactos",
|
||||
"Conversation": "Conversación",
|
||||
"Reply": "Responder",
|
||||
"Repost": "Citar nota",
|
||||
@ -63,6 +64,7 @@
|
||||
},
|
||||
"menuItems": {
|
||||
"relays": "Relays",
|
||||
"contacts": "Contactos",
|
||||
"notConnected": "No conectado",
|
||||
"connectedRelays": "{{number}} conectados",
|
||||
"following": "{{following}} siguiendo",
|
||||
@ -135,7 +137,7 @@
|
||||
"loginMethod": "Acceder con",
|
||||
"mnemonic": "Mnemónico"
|
||||
},
|
||||
"contactsFeed": {
|
||||
"contactsPage": {
|
||||
"notifications": {
|
||||
"keyCopied": "Clave pública copiada.",
|
||||
"contactAdded": "Perfil seguido.",
|
||||
|
@ -17,6 +17,7 @@
|
||||
"ProfileConnect": "",
|
||||
"ProfileLoad": "",
|
||||
"Landing": "",
|
||||
"Contacts": "Contacts",
|
||||
"Conversation": "Conversation",
|
||||
"Reply": "Répondre",
|
||||
"Repost": "Citer la note",
|
||||
@ -63,6 +64,7 @@
|
||||
},
|
||||
"menuItems": {
|
||||
"relays": "Relais",
|
||||
"contacts": "Contacts",
|
||||
"notConnected": "Non connecté",
|
||||
"connectedRelays": "{{number}} connecté",
|
||||
"following": "{{following}} abonnements",
|
||||
@ -141,7 +143,7 @@
|
||||
"loginMethod": "Login with",
|
||||
"mnemonic": "Mnemonic"
|
||||
},
|
||||
"contactsFeed": {
|
||||
"contactsPage": {
|
||||
"notifications": {
|
||||
"keyCopied": "Clé publique copiée.",
|
||||
"contactAdded": "Profil suivi.",
|
||||
|
@ -17,6 +17,7 @@
|
||||
"ProfileConnect": "",
|
||||
"ProfileLoad": "",
|
||||
"Landing": "",
|
||||
"Contacts": "Contacts",
|
||||
"Conversation": "Диалог",
|
||||
"Reply": "Ответ",
|
||||
"Note": "Заметка",
|
||||
@ -63,6 +64,7 @@
|
||||
},
|
||||
"menuItems": {
|
||||
"relays": "Реле",
|
||||
"contacts": "Contacts",
|
||||
"notConnected": "Not connected",
|
||||
"connectedRelays": "{{number}} подключено",
|
||||
"following": "{{following}} following",
|
||||
@ -135,7 +137,7 @@
|
||||
"loginMethod": "Login with",
|
||||
"mnemonic": "Mnemonic"
|
||||
},
|
||||
"contactsFeed": {
|
||||
"contactsPage": {
|
||||
"notifications": {
|
||||
"keyCopied": "Публичный ключ скопирован.",
|
||||
"contactAdded": "Вы подписались.",
|
||||
|
@ -15,6 +15,7 @@
|
||||
"homeNavigator": {
|
||||
"ProfileCreate": "创建用户",
|
||||
"ProfileConnect": "",
|
||||
"Contacts": "Contacts",
|
||||
"ProfileLoad": "",
|
||||
"Landing": "",
|
||||
"Conversation": "会话",
|
||||
@ -47,12 +48,12 @@
|
||||
"isContact": "正在关注",
|
||||
"isNotContact": "关注",
|
||||
"contentWarning": "敏感内容",
|
||||
"send": "发送",
|
||||
"send": "发送"
|
||||
},
|
||||
"uploadImage": {
|
||||
"notifications": {
|
||||
"imageUploaded": "您的图片已上传\n请赞助我们的服务:",
|
||||
"imageUploadErro": "上传图片出现错误",
|
||||
"imageUploadErro": "上传图片出现错误"
|
||||
},
|
||||
"uploadImageWarningTitle": "Important",
|
||||
"uploadImageWarning": "Your image will be uploaded to a public hosting service and will be visible for anyone.",
|
||||
@ -61,6 +62,7 @@
|
||||
"poweredBy": "由{{uri}}提供支持"
|
||||
},
|
||||
"menuItems": {
|
||||
"contacts": "Contacts",
|
||||
"relays": "中继",
|
||||
"notConnected": "未连接",
|
||||
"connectedRelays": "已连接 {{number}} 个中继",
|
||||
@ -133,7 +135,7 @@
|
||||
"loginMethod": "登入",
|
||||
"mnemonic": "助记词"
|
||||
},
|
||||
"contactsFeed": {
|
||||
"contactsPage": {
|
||||
"notifications": {
|
||||
"keyCopied": "已复制公钥",
|
||||
"contactAdded": "已关注",
|
||||
|
@ -1,5 +1,5 @@
|
||||
import React, { useContext, useEffect, useState } from 'react'
|
||||
import { Dimensions, NativeScrollEvent, NativeSyntheticEvent, StyleSheet, View } from 'react-native'
|
||||
import { NativeScrollEvent, NativeSyntheticEvent, StyleSheet, View } from 'react-native'
|
||||
import Clipboard from '@react-native-clipboard/clipboard'
|
||||
import { AppContext } from '../../Contexts/AppContext'
|
||||
import { Kind } from 'nostr-tools'
|
||||
@ -33,7 +33,7 @@ import ProfileData from '../../Components/ProfileData'
|
||||
import { handleInfinityScroll } from '../../Functions/NativeFunctions'
|
||||
import { queryProfile } from 'nostr-tools/nip05'
|
||||
|
||||
export const ContactsFeed: React.FC = () => {
|
||||
export const ContactsPage: React.FC = () => {
|
||||
const { t } = useTranslation('common')
|
||||
const initialPageSize = 20
|
||||
const { database, setDisplayUserDrawer } = useContext(AppContext)
|
||||
@ -203,7 +203,7 @@ export const ContactsFeed: React.FC = () => {
|
||||
</View>
|
||||
<View>
|
||||
<Button onPress={() => (item.contact ? removeContact(item) : addContact(item))}>
|
||||
{item.contact ? t('contactsFeed.stopFollowing') : t('contactsFeed.follow')}
|
||||
{item.contact ? t('contactsPage.stopFollowing') : t('contactsPage.follow')}
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
@ -232,7 +232,7 @@ export const ContactsFeed: React.FC = () => {
|
||||
/>
|
||||
</View>
|
||||
<View>
|
||||
<Button onPress={() => unblock(item)}>{t('contactsFeed.unblock')}</Button>
|
||||
<Button onPress={() => unblock(item)}>{t('contactsPage.unblock')}</Button>
|
||||
</View>
|
||||
</View>
|
||||
</TouchableRipple>
|
||||
@ -269,13 +269,13 @@ export const ContactsFeed: React.FC = () => {
|
||||
color={theme.colors.onPrimaryContainer}
|
||||
/>
|
||||
<Text variant='headlineSmall' style={styles.center}>
|
||||
{t('contactsFeed.emptyTitleFollowing')}
|
||||
{t('contactsPage.emptyTitleFollowing')}
|
||||
</Text>
|
||||
<Text variant='bodyMedium' style={styles.center}>
|
||||
{t('contactsFeed.emptyDescriptionFollowing')}
|
||||
{t('contactsPage.emptyDescriptionFollowing')}
|
||||
</Text>
|
||||
<Button mode='contained' compact onPress={() => bottomSheetAddContactRef.current?.open()}>
|
||||
{t('contactsFeed.emptyButtonFollowing')}
|
||||
{t('contactsPage.emptyButtonFollowing')}
|
||||
</Button>
|
||||
</View>
|
||||
)
|
||||
@ -288,6 +288,7 @@ export const ContactsFeed: React.FC = () => {
|
||||
data={following.slice(0, pageSize)}
|
||||
renderItem={renderContactItem}
|
||||
onScroll={onScroll}
|
||||
ItemSeparatorComponent={Divider}
|
||||
ListEmptyComponent={ListEmptyComponentFollowing}
|
||||
horizontal={false}
|
||||
/>
|
||||
@ -303,10 +304,10 @@ export const ContactsFeed: React.FC = () => {
|
||||
color={theme.colors.onPrimaryContainer}
|
||||
/>
|
||||
<Text variant='headlineSmall' style={styles.center}>
|
||||
{t('contactsFeed.emptyTitleFollower')}
|
||||
{t('contactsPage.emptyTitleFollower')}
|
||||
</Text>
|
||||
<Text variant='bodyMedium' style={styles.center}>
|
||||
{t('contactsFeed.emptyDescriptionFollower')}
|
||||
{t('contactsPage.emptyDescriptionFollower')}
|
||||
</Text>
|
||||
<Button
|
||||
mode='contained'
|
||||
@ -316,7 +317,7 @@ export const ContactsFeed: React.FC = () => {
|
||||
Clipboard.setString(nPub ?? '')
|
||||
}}
|
||||
>
|
||||
{t('contactsFeed.emptyButtonFollower')}
|
||||
{t('contactsPage.emptyButtonFollower')}
|
||||
</Button>
|
||||
</View>
|
||||
)
|
||||
@ -345,10 +346,10 @@ export const ContactsFeed: React.FC = () => {
|
||||
color={theme.colors.onPrimaryContainer}
|
||||
/>
|
||||
<Text variant='headlineSmall' style={styles.center}>
|
||||
{t('contactsFeed.emptyTitleBlocked')}
|
||||
{t('contactsPage.emptyTitleBlocked')}
|
||||
</Text>
|
||||
<Text variant='bodyMedium' style={styles.center}>
|
||||
{t('contactsFeed.emptyDescriptionBlocked')}
|
||||
{t('contactsPage.emptyDescriptionBlocked')}
|
||||
</Text>
|
||||
</View>
|
||||
)
|
||||
@ -361,6 +362,7 @@ export const ContactsFeed: React.FC = () => {
|
||||
data={blocked.slice(0, pageSize)}
|
||||
renderItem={renderBlockedItem}
|
||||
onScroll={onScroll}
|
||||
ItemSeparatorComponent={Divider}
|
||||
ListEmptyComponent={ListEmptyComponentBlocked}
|
||||
horizontal={false}
|
||||
/>
|
||||
@ -386,7 +388,7 @@ export const ContactsFeed: React.FC = () => {
|
||||
>
|
||||
<TouchableRipple style={styles.textWrapper} onPress={() => setTabKey('following')}>
|
||||
<Text style={styles.tabText}>
|
||||
{t('contactsFeed.following', { count: following.length })}
|
||||
{t('contactsPage.following', { count: following.length })}
|
||||
</Text>
|
||||
</TouchableRipple>
|
||||
</View>
|
||||
@ -400,7 +402,7 @@ export const ContactsFeed: React.FC = () => {
|
||||
>
|
||||
<TouchableRipple style={styles.textWrapper} onPress={() => setTabKey('followers')}>
|
||||
<Text style={styles.tabText}>
|
||||
{t('contactsFeed.followers', { count: followers.length })}
|
||||
{t('contactsPage.followers', { count: followers.length })}
|
||||
</Text>
|
||||
</TouchableRipple>
|
||||
</View>
|
||||
@ -414,7 +416,7 @@ export const ContactsFeed: React.FC = () => {
|
||||
>
|
||||
<TouchableRipple style={styles.textWrapper} onPress={() => setTabKey('blocked')}>
|
||||
<Text style={styles.tabText}>
|
||||
{t('contactsFeed.blocked', { count: blocked.length })}
|
||||
{t('contactsPage.blocked', { count: blocked.length })}
|
||||
</Text>
|
||||
</TouchableRipple>
|
||||
</View>
|
||||
@ -422,7 +424,7 @@ export const ContactsFeed: React.FC = () => {
|
||||
{renderScene[tabKey]}
|
||||
{privateKey && (
|
||||
<AnimatedFAB
|
||||
style={[styles.fab, { top: Dimensions.get('window').height - 216 }]}
|
||||
style={styles.fab}
|
||||
icon='account-multiple-plus-outline'
|
||||
label='Label'
|
||||
onPress={() => bottomSheetAddContactRef.current?.open()}
|
||||
@ -438,12 +440,12 @@ export const ContactsFeed: React.FC = () => {
|
||||
onClose={() => setContactInput('')}
|
||||
>
|
||||
<View>
|
||||
<Text variant='titleLarge'>{t('contactsFeed.addContactTitle')}</Text>
|
||||
<Text variant='bodyMedium'>{t('contactsFeed.addContactDescription')}</Text>
|
||||
<Text variant='titleLarge'>{t('contactsPage.addContactTitle')}</Text>
|
||||
<Text variant='bodyMedium'>{t('contactsPage.addContactDescription')}</Text>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
mode='outlined'
|
||||
label={t('contactsFeed.addContact') ?? ''}
|
||||
label={t('contactsPage.addContact') ?? ''}
|
||||
onChangeText={setContactInput}
|
||||
value={contactInput}
|
||||
right={
|
||||
@ -461,7 +463,7 @@ export const ContactsFeed: React.FC = () => {
|
||||
onPress={onPressAddContact}
|
||||
loading={isAddingContact}
|
||||
>
|
||||
{t('contactsFeed.addContact')}
|
||||
{t('contactsPage.addContact')}
|
||||
</Button>
|
||||
<Button
|
||||
mode='outlined'
|
||||
@ -470,7 +472,7 @@ export const ContactsFeed: React.FC = () => {
|
||||
setContactInput('')
|
||||
}}
|
||||
>
|
||||
{t('contactsFeed.cancel')}
|
||||
{t('contactsPage.cancel')}
|
||||
</Button>
|
||||
</View>
|
||||
</RBSheet>
|
||||
@ -482,7 +484,7 @@ export const ContactsFeed: React.FC = () => {
|
||||
onIconPress={() => setShowNotification(undefined)}
|
||||
onDismiss={() => setShowNotification(undefined)}
|
||||
>
|
||||
{t(`contactsFeed.notifications.${showNotification}`)}
|
||||
{t(`contactsPage.notifications.${showNotification}`)}
|
||||
</Snackbar>
|
||||
)}
|
||||
</>
|
||||
@ -531,9 +533,7 @@ const styles = StyleSheet.create({
|
||||
marginBottom: 95,
|
||||
},
|
||||
contactRow: {
|
||||
paddingLeft: 16,
|
||||
paddingRight: 16,
|
||||
paddingTop: 16,
|
||||
padding: 16,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
@ -548,6 +548,7 @@ const styles = StyleSheet.create({
|
||||
alignContent: 'center',
|
||||
},
|
||||
fab: {
|
||||
bottom: 65,
|
||||
right: 16,
|
||||
position: 'absolute',
|
||||
},
|
||||
@ -582,4 +583,4 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
})
|
||||
|
||||
export default ContactsFeed
|
||||
export default ContactsPage
|
@ -37,6 +37,7 @@ export const ConversationPage: React.FC<ConversationPageProps> = ({ route }) =>
|
||||
const { publicKey, privateKey, name, picture } = useContext(UserContext)
|
||||
const otherPubKey = useMemo(() => route.params.pubKey, [])
|
||||
const [pageSize, setPageSize] = useState<number>(initialPageSize)
|
||||
const [decryptedMessages, setDecryptedMessages] = useState<Record<string, string>>({})
|
||||
const [directMessages, setDirectMessages] = useState<DirectMessage[]>([])
|
||||
const [sendingMessages, setSendingMessages] = useState<DirectMessage[]>([])
|
||||
const [otherUser, setOtherUser] = useState<User>({ id: otherPubKey })
|
||||
@ -74,16 +75,22 @@ export const ConversationPage: React.FC<ConversationPageProps> = ({ route }) =>
|
||||
}).then((results) => {
|
||||
if (results.length > 0) {
|
||||
setSendingMessages([])
|
||||
setDirectMessages((prev) => {
|
||||
return results.map((message, index) => {
|
||||
if (prev.length > index) {
|
||||
return prev[index]
|
||||
setDirectMessages(
|
||||
results.map((message) => {
|
||||
if (message?.id) {
|
||||
if (decryptedMessages[message.id]) {
|
||||
message.content = decryptedMessages[message.id]
|
||||
} else {
|
||||
message.content = decrypt(privateKey, otherPubKey, message.content ?? '')
|
||||
return message
|
||||
setDecryptedMessages((prev) => {
|
||||
if (message?.id) prev[message.id] = message.content
|
||||
return prev
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
return message
|
||||
}),
|
||||
)
|
||||
if (subscribe) subscribeDirectMessages(results[0].created_at)
|
||||
} else if (subscribe) {
|
||||
subscribeDirectMessages()
|
||||
|
@ -349,9 +349,7 @@ const styles = StyleSheet.create({
|
||||
flex: 1,
|
||||
},
|
||||
contactRow: {
|
||||
paddingLeft: 16,
|
||||
paddingRight: 16,
|
||||
paddingTop: 16,
|
||||
padding: 16,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
width: '100%',
|
||||
|
@ -20,6 +20,9 @@ import { AppContext } from '../../Contexts/AppContext'
|
||||
import RelayCard from '../../Components/RelayCard'
|
||||
import { updateAllRead } from '../../Functions/DatabaseFunctions/DirectMessages'
|
||||
import { getUnixTime } from 'date-fns'
|
||||
import ContactsPage from '../ContactsPage'
|
||||
import GroupPage from '../GroupPage'
|
||||
import GroupHeaderIcon from '../../Components/GroupHeaderIcon'
|
||||
|
||||
export const HomeNavigator: React.FC = () => {
|
||||
const theme = useTheme()
|
||||
@ -118,6 +121,9 @@ 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}/>
|
||||
)}
|
||||
</Appbar.Header>
|
||||
)
|
||||
},
|
||||
@ -131,8 +137,10 @@ 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.Group>
|
||||
<Stack.Group>
|
||||
<Stack.Screen name='Contacts' component={ContactsPage} />
|
||||
<Stack.Screen name='Relays' component={RelaysPage} />
|
||||
<Stack.Screen name='About' component={AboutPage} />
|
||||
<Stack.Screen name='Config' component={ConfigPage} />
|
||||
|
347
frontend/Pages/GroupPage/index.tsx
Normal file
347
frontend/Pages/GroupPage/index.tsx
Normal file
@ -0,0 +1,347 @@
|
||||
import React, { useContext, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { NativeScrollEvent, NativeSyntheticEvent, ScrollView, StyleSheet, View } from 'react-native'
|
||||
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 { 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 { UserContext } from '../../Contexts/UserContext'
|
||||
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons'
|
||||
import { useFocusEffect } from '@react-navigation/native'
|
||||
import { Kind } from 'nostr-tools'
|
||||
import { handleInfinityScroll } from '../../Functions/NativeFunctions'
|
||||
import NostrosAvatar from '../../Components/NostrosAvatar'
|
||||
import { FlashList, ListRenderItem } from '@shopify/flash-list'
|
||||
import UploadImage from '../../Components/UploadImage'
|
||||
|
||||
interface GroupPageProps {
|
||||
route: { params: { groupId: string } }
|
||||
}
|
||||
|
||||
export const GroupPage: React.FC<GroupPageProps> = ({ route }) => {
|
||||
const initialPageSize = 10
|
||||
const theme = useTheme()
|
||||
const scrollViewRef = useRef<ScrollView>()
|
||||
const { database, setRefreshBottomBarAt, setDisplayUserDrawer } = useContext(AppContext)
|
||||
const { relayPool, lastEventId } = useContext(RelayPoolContext)
|
||||
const { publicKey, privateKey, name, picture } = 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 [input, setInput] = useState<string>('')
|
||||
const [showNotification, setShowNotification] = useState<string>()
|
||||
const [startUpload, setStartUpload] = useState<boolean>(false)
|
||||
const [uploadingFile, setUploadingFile] = useState<boolean>(false)
|
||||
|
||||
const { t } = useTranslation('common')
|
||||
|
||||
useFocusEffect(
|
||||
React.useCallback(() => {
|
||||
loadDirectMessages(true)
|
||||
subscribeDirectMessages()
|
||||
|
||||
return () => relayPool?.unsubscribe([`conversation${route.params.groupId}`])
|
||||
}, []),
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
loadDirectMessages(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, {
|
||||
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)
|
||||
} else if (subscribe) {
|
||||
subscribeDirectMessages()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const subscribeDirectMessages: (lastCreateAt?: number) => void = async (lastCreateAt) => {
|
||||
if (publicKey && otherPubKey) {
|
||||
relayPool?.subscribe(`conversation${route.params.groupId}`, [
|
||||
{
|
||||
kinds: [Kind.EncryptedDirectMessage],
|
||||
authors: [publicKey],
|
||||
'#p': [otherPubKey],
|
||||
since: lastCreateAt ?? 0,
|
||||
},
|
||||
{
|
||||
kinds: [Kind.EncryptedDirectMessage],
|
||||
authors: [otherPubKey],
|
||||
'#p': [publicKey],
|
||||
since: lastCreateAt ?? 0,
|
||||
},
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
const send: () => void = () => {
|
||||
if (input !== '' && otherPubKey && publicKey && privateKey) {
|
||||
const event: Event = {
|
||||
content: input,
|
||||
created_at: getUnixTime(new Date()),
|
||||
kind: Kind.EncryptedDirectMessage,
|
||||
pubkey: publicKey,
|
||||
tags: usersToTags([otherUser]),
|
||||
}
|
||||
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])
|
||||
setInput('')
|
||||
}
|
||||
}
|
||||
|
||||
const renderDirectMessageItem: ListRenderItem<DirectMessage> = ({ index, item }) => {
|
||||
if (!publicKey || !privateKey || !otherUser) return <></>
|
||||
|
||||
const displayName =
|
||||
item.pubkey === publicKey ? usernamePubKey(name, publicKey) : username(otherUser)
|
||||
const showAvatar = directMessages[index - 1]?.pubkey !== item.pubkey
|
||||
|
||||
return (
|
||||
<View style={styles.messageRow}>
|
||||
{publicKey !== item.pubkey && (
|
||||
<View style={styles.pictureSpaceLeft}>
|
||||
{showAvatar && (
|
||||
<TouchableRipple onPress={() => setDisplayUserDrawer(otherPubKey)}>
|
||||
<NostrosAvatar
|
||||
name={otherUser.name}
|
||||
pubKey={otherPubKey}
|
||||
src={otherUser.picture}
|
||||
size={40}
|
||||
/>
|
||||
</TouchableRipple>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
<Card
|
||||
style={[
|
||||
styles.card,
|
||||
// FIXME: can't find this color
|
||||
{
|
||||
backgroundColor:
|
||||
publicKey === item.pubkey ? theme.colors.tertiaryContainer : '#001C37',
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Card.Content>
|
||||
<View style={styles.cardContentInfo}>
|
||||
<Text>{displayName}</Text>
|
||||
<View style={styles.cardContentDate}>
|
||||
{item.pending && (
|
||||
<View style={styles.cardContentPending}>
|
||||
<MaterialCommunityIcons
|
||||
name='clock-outline'
|
||||
size={14}
|
||||
color={theme.colors.onPrimaryContainer}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
<Text>
|
||||
{formatDistance(fromUnixTime(item.created_at), new Date(), { addSuffix: true })}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
<TextContent content={item.content} />
|
||||
</Card.Content>
|
||||
</Card>
|
||||
{publicKey === item.pubkey && (
|
||||
<View style={styles.pictureSpaceRight}>
|
||||
{showAvatar && (
|
||||
<TouchableRipple onPress={() => setDisplayUserDrawer(publicKey)}>
|
||||
<NostrosAvatar name={name} pubKey={publicKey} src={picture} size={40} />
|
||||
</TouchableRipple>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const onScroll: (event: NativeSyntheticEvent<NativeScrollEvent>) => void = (event) => {
|
||||
if (handleInfinityScroll(event)) {
|
||||
setPageSize(pageSize + initialPageSize)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<FlashList
|
||||
inverted
|
||||
data={[...sendingMessages, ...directMessages]}
|
||||
renderItem={renderDirectMessageItem}
|
||||
horizontal={false}
|
||||
ref={scrollViewRef}
|
||||
estimatedItemSize={100}
|
||||
onScroll={onScroll}
|
||||
/>
|
||||
<View
|
||||
style={[
|
||||
styles.input,
|
||||
{
|
||||
backgroundColor: '#001C37',
|
||||
},
|
||||
]}
|
||||
>
|
||||
<TextInput
|
||||
mode='outlined'
|
||||
multiline
|
||||
label={t('conversationPage.typeMessage') ?? ''}
|
||||
value={input}
|
||||
onChangeText={setInput}
|
||||
onFocus={() => scrollViewRef.current?.scrollToEnd({ animated: true })}
|
||||
left={
|
||||
<TextInput.Icon
|
||||
icon={() => (
|
||||
<MaterialCommunityIcons
|
||||
name='image-outline'
|
||||
size={25}
|
||||
color={theme.colors.onPrimaryContainer}
|
||||
/>
|
||||
)}
|
||||
onPress={() => setStartUpload(true)}
|
||||
/>
|
||||
}
|
||||
right={
|
||||
<TextInput.Icon
|
||||
icon={() => (
|
||||
<MaterialCommunityIcons
|
||||
name='send-outline'
|
||||
size={25}
|
||||
color={theme.colors.onPrimaryContainer}
|
||||
/>
|
||||
)}
|
||||
onPress={send}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</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) => {
|
||||
setInput((prev) => `${prev} ${imageUri}`)
|
||||
setStartUpload(false)
|
||||
}}
|
||||
uploadingFile={uploadingFile}
|
||||
setUploadingFile={setUploadingFile}
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
scrollView: {
|
||||
paddingBottom: 16,
|
||||
},
|
||||
messageRow: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
cardContentDate: {
|
||||
flexDirection: 'row',
|
||||
},
|
||||
container: {
|
||||
padding: 16,
|
||||
justifyContent: 'space-between',
|
||||
flex: 1,
|
||||
},
|
||||
card: {
|
||||
marginTop: 16,
|
||||
flex: 6,
|
||||
},
|
||||
cardContentInfo: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
cardContentPending: {
|
||||
flexDirection: 'row',
|
||||
alignContent: 'center',
|
||||
justifyContent: 'center',
|
||||
paddingRight: 5,
|
||||
paddingTop: 3,
|
||||
},
|
||||
pictureSpaceLeft: {
|
||||
justifyContent: 'flex-end',
|
||||
width: 50,
|
||||
flex: 1,
|
||||
},
|
||||
pictureSpaceRight: {
|
||||
alignContent: 'flex-end',
|
||||
justifyContent: 'flex-end',
|
||||
width: 50,
|
||||
flex: 1,
|
||||
paddingLeft: 16,
|
||||
},
|
||||
input: {
|
||||
flexDirection: 'column-reverse',
|
||||
marginTop: 16,
|
||||
},
|
||||
snackbar: {
|
||||
margin: 16,
|
||||
bottom: 70,
|
||||
},
|
||||
})
|
||||
|
||||
export default GroupPage
|
295
frontend/Pages/GroupsFeed/index.tsx
Normal file
295
frontend/Pages/GroupsFeed/index.tsx
Normal file
@ -0,0 +1,295 @@
|
||||
import React, { useContext, useEffect, useState } from 'react'
|
||||
import { Dimensions, StyleSheet, View } from 'react-native'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
AnimatedFAB,
|
||||
Button,
|
||||
Divider,
|
||||
Text,
|
||||
TextInput,
|
||||
TouchableRipple,
|
||||
useTheme,
|
||||
} from 'react-native-paper'
|
||||
import RBSheet from 'react-native-raw-bottom-sheet'
|
||||
import UploadImage from '../../Components/UploadImage'
|
||||
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons'
|
||||
import { Kind } from 'nostr-tools'
|
||||
import { Event } from '../../lib/nostr/Events'
|
||||
import { UserContext } from '../../Contexts/UserContext'
|
||||
import { RelayPoolContext } from '../../Contexts/RelayPoolContext'
|
||||
import { getUnixTime } from 'date-fns'
|
||||
import { useFocusEffect } from '@react-navigation/native'
|
||||
import { AppContext } from '../../Contexts/AppContext'
|
||||
import { getGroups, Group } from '../../Functions/DatabaseFunctions/Groups'
|
||||
import { formatId, username } from '../../Functions/RelayFunctions/Users'
|
||||
import NostrosAvatar from '../../Components/NostrosAvatar'
|
||||
import { navigate } from '../../lib/Navigation'
|
||||
import { FlashList, ListRenderItem } from '@shopify/flash-list'
|
||||
|
||||
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 bottomSheetCreateRef = React.useRef<RBSheet>(null)
|
||||
const [groups, setGroups] = 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)
|
||||
|
||||
useFocusEffect(
|
||||
React.useCallback(() => {
|
||||
loadGroups()
|
||||
subscribeGroups()
|
||||
|
||||
return () => relayPool?.unsubscribe(['groups-create', 'groups-meta', 'groups-messages'])
|
||||
}, []),
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
loadGroups()
|
||||
}, [lastEventId])
|
||||
|
||||
const subscribeGroups: () => void = async () => {
|
||||
if (publicKey) {
|
||||
relayPool?.subscribe('groups-create', [
|
||||
{
|
||||
kinds: [Kind.ChannelCreation],
|
||||
authors: [publicKey],
|
||||
},
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
const loadGroups: () => void = () => {
|
||||
if (database && publicKey) {
|
||||
getGroups(database).then((results) => {
|
||||
if (results && results.length > 0) {
|
||||
setGroups(results)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const createNewGroup: () => void = () => {
|
||||
if (newGroupName && publicKey) {
|
||||
const event: Event = {
|
||||
content: JSON.stringify({
|
||||
name: newGroupName,
|
||||
about: newGroupDescription,
|
||||
picture: newGroupPicture,
|
||||
}),
|
||||
created_at: getUnixTime(new Date()),
|
||||
kind: Kind.ChannelCreation,
|
||||
pubkey: publicKey,
|
||||
tags: [],
|
||||
}
|
||||
relayPool?.sendEvent(event)
|
||||
bottomSheetCreateRef.current?.close()
|
||||
}
|
||||
}
|
||||
|
||||
const renderGroupItem: ListRenderItem<Group> = ({ item }) => {
|
||||
return (
|
||||
<TouchableRipple
|
||||
onPress={() =>
|
||||
navigate('GroupPage', {
|
||||
groupId: item.id,
|
||||
title: item.name || formatId(item.id),
|
||||
})
|
||||
}
|
||||
>
|
||||
<View style={styles.row}>
|
||||
<View style={styles.groupData}>
|
||||
<NostrosAvatar src={item.picture} name={item.name} pubKey={item.pubkey} />
|
||||
<View style={styles.groupDescription}>
|
||||
<Text>
|
||||
{item.name && item.name?.length > 30 ? `${item.name?.slice(0, 30)}...` : item.name}
|
||||
</Text>
|
||||
<Text style={{ color: theme.colors.onSurfaceVariant }}>
|
||||
{item.about && item.about?.length > 30
|
||||
? `${item.about?.slice(0, 30)}...`
|
||||
: item.about}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View>
|
||||
<View style={styles.username}>
|
||||
<Text>{username({ name: item.user_name, id: item.pubkey })}</Text>
|
||||
{item.valid_nip05 && (
|
||||
<MaterialCommunityIcons
|
||||
name='check-decagram-outline'
|
||||
color={theme.colors.onPrimaryContainer}
|
||||
style={styles.verifyIcon}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
<Text style={{ color: theme.colors.onSurfaceVariant }}>{formatId(item.id)}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</TouchableRipple>
|
||||
)
|
||||
}
|
||||
|
||||
const bottomSheetStyles = React.useMemo(() => {
|
||||
return {
|
||||
container: {
|
||||
backgroundColor: theme.colors.background,
|
||||
paddingTop: 16,
|
||||
paddingRight: 16,
|
||||
paddingBottom: 32,
|
||||
paddingLeft: 16,
|
||||
borderTopRightRadius: 28,
|
||||
borderTopLeftRadius: 28,
|
||||
height: 'auto',
|
||||
},
|
||||
}
|
||||
}, [])
|
||||
|
||||
const ListEmptyComponentBlocked = (
|
||||
<View style={styles.blankNoButton}>
|
||||
<MaterialCommunityIcons
|
||||
name='account-group-outline'
|
||||
size={64}
|
||||
style={styles.center}
|
||||
color={theme.colors.onPrimaryContainer}
|
||||
/>
|
||||
<Text variant='headlineSmall' style={styles.center}>
|
||||
{t('contactsPage.emptyTitleBlocked')}
|
||||
</Text>
|
||||
<Text variant='bodyMedium' style={styles.center}>
|
||||
{t('contactsPage.emptyDescriptionBlocked')}
|
||||
</Text>
|
||||
</View>
|
||||
)
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<FlashList
|
||||
estimatedItemSize={71}
|
||||
showsVerticalScrollIndicator={false}
|
||||
data={groups}
|
||||
renderItem={renderGroupItem}
|
||||
ItemSeparatorComponent={Divider}
|
||||
ListEmptyComponent={ListEmptyComponentBlocked}
|
||||
horizontal={false}
|
||||
/>
|
||||
<AnimatedFAB
|
||||
style={[styles.fab, { top: Dimensions.get('window').height - 216 }]}
|
||||
icon='plus'
|
||||
label='Label'
|
||||
onPress={() => bottomSheetCreateRef.current?.open()}
|
||||
animateFrom='right'
|
||||
iconMode='static'
|
||||
extended={false}
|
||||
/>
|
||||
<RBSheet ref={bottomSheetCreateRef} closeOnDragDown={true} customStyles={bottomSheetStyles}>
|
||||
<View>
|
||||
<Text style={styles.input} variant='titleLarge'>
|
||||
{t('groupsFeed.createTitle')}
|
||||
</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}
|
||||
multiline
|
||||
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)}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Button mode='contained' disabled={!newGroupName} onPress={createNewGroup}>
|
||||
{t('groupsFeed.newGroupCreate')}
|
||||
</Button>
|
||||
<UploadImage
|
||||
startUpload={startUpload}
|
||||
setImageUri={(imageUri) => {
|
||||
setNewGroupPicture(imageUri)
|
||||
setStartUpload(false)
|
||||
}}
|
||||
uploadingFile={uploadingFile}
|
||||
setUploadingFile={setUploadingFile}
|
||||
/>
|
||||
</View>
|
||||
</RBSheet>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
},
|
||||
blankNoButton: {
|
||||
justifyContent: 'space-between',
|
||||
height: 192,
|
||||
marginTop: 75,
|
||||
padding: 16,
|
||||
},
|
||||
fab: {
|
||||
right: 16,
|
||||
position: 'absolute',
|
||||
},
|
||||
groupDescription: {
|
||||
paddingLeft: 16,
|
||||
},
|
||||
input: {
|
||||
marginBottom: 16,
|
||||
},
|
||||
verifyIcon: {
|
||||
paddingTop: 4,
|
||||
paddingLeft: 5,
|
||||
},
|
||||
list: {
|
||||
paddingLeft: 16,
|
||||
paddingRight: 16,
|
||||
},
|
||||
containerAvatar: {
|
||||
marginTop: 10,
|
||||
},
|
||||
row: {
|
||||
padding: 16,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
groupData: {
|
||||
flexDirection: 'row',
|
||||
},
|
||||
username: {
|
||||
flexDirection: 'row',
|
||||
},
|
||||
center: {
|
||||
alignContent: 'center',
|
||||
textAlign: 'center',
|
||||
},
|
||||
})
|
||||
|
||||
export default GroupsFeed
|
@ -1,6 +1,5 @@
|
||||
import React, { useContext, useEffect, useState } from 'react'
|
||||
import { Badge, Button, Text, TouchableRipple, useTheme } from 'react-native-paper'
|
||||
import ContactsFeed from '../ContactsFeed'
|
||||
import ConversationsFeed from '../ConversationsFeed'
|
||||
import HomeFeed from '../HomeFeed'
|
||||
import NotificationsFeed from '../NotificationsFeed'
|
||||
@ -20,6 +19,7 @@ import {
|
||||
getDirectMessagesCount,
|
||||
getGroupedDirectMessages,
|
||||
} from '../../Functions/DatabaseFunctions/DirectMessages'
|
||||
import GroupsFeed from '../GroupsFeed'
|
||||
|
||||
export const HomePage: React.FC = () => {
|
||||
const theme = useTheme()
|
||||
@ -143,6 +143,20 @@ export const HomePage: React.FC = () => {
|
||||
}}
|
||||
/>
|
||||
{privateKey && (
|
||||
<>
|
||||
<Tab.Screen
|
||||
name='groups'
|
||||
component={GroupsFeed}
|
||||
options={{
|
||||
tabBarIcon: ({ focused, size }) => (
|
||||
<MaterialCommunityIcons
|
||||
name={focused ? 'account-group' : 'account-group-outline'}
|
||||
size={size}
|
||||
color={theme.colors.onPrimaryContainer}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<Tab.Screen
|
||||
name='messages'
|
||||
component={ConversationsFeed}
|
||||
@ -161,20 +175,8 @@ export const HomePage: React.FC = () => {
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<Tab.Screen
|
||||
name='contacts'
|
||||
component={ContactsFeed}
|
||||
options={{
|
||||
tabBarIcon: ({ focused, size }) => (
|
||||
<MaterialCommunityIcons
|
||||
name={focused ? 'account-group' : 'account-group-outline'}
|
||||
size={size}
|
||||
color={theme.colors.onPrimaryContainer}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<Tab.Screen
|
||||
name='notifications'
|
||||
component={NotificationsFeed}
|
||||
|
@ -21,8 +21,11 @@ import RBSheet from 'react-native-raw-bottom-sheet'
|
||||
import NostrosAvatar from '../../Components/NostrosAvatar'
|
||||
import { getUnixTime } from 'date-fns'
|
||||
import { useFocusEffect } from '@react-navigation/native'
|
||||
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons'
|
||||
import UploadImage from '../../Components/UploadImage'
|
||||
|
||||
export const ProfileConfigPage: React.FC = () => {
|
||||
const { t } = useTranslation('common')
|
||||
const theme = useTheme()
|
||||
const bottomSheetPictureRef = React.useRef<RBSheet>(null)
|
||||
const bottomSheetDirectoryRef = React.useRef<RBSheet>(null)
|
||||
@ -49,7 +52,8 @@ export const ProfileConfigPage: React.FC = () => {
|
||||
// State
|
||||
const [showNotification, setShowNotification] = useState<undefined | string>()
|
||||
const [isPublishingProfile, setIsPublishingProfile] = useState<string>()
|
||||
const { t } = useTranslation('common')
|
||||
const [startUpload, setStartUpload] = useState<boolean>(false)
|
||||
const [uploadingFile, setUploadingFile] = useState<boolean>(false)
|
||||
|
||||
useFocusEffect(
|
||||
React.useCallback(() => {
|
||||
@ -291,6 +295,18 @@ export const ProfileConfigPage: React.FC = () => {
|
||||
forceTextInputFocus={false}
|
||||
/>
|
||||
}
|
||||
left={
|
||||
<TextInput.Icon
|
||||
icon={() => (
|
||||
<MaterialCommunityIcons
|
||||
name='image-outline'
|
||||
size={25}
|
||||
color={theme.colors.onPrimaryContainer}
|
||||
/>
|
||||
)}
|
||||
onPress={() => setStartUpload(true)}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Button
|
||||
mode='contained'
|
||||
@ -405,6 +421,16 @@ export const ProfileConfigPage: React.FC = () => {
|
||||
{t(`profileConfigPage.notifications.${showNotification}`, { nip05, lud06: lnurl })}
|
||||
</Snackbar>
|
||||
)}
|
||||
<UploadImage
|
||||
startUpload={startUpload}
|
||||
setImageUri={(imageUri) => {
|
||||
setPicture(imageUri)
|
||||
setStartUpload(false)
|
||||
}}
|
||||
uploadingFile={uploadingFile}
|
||||
setUploadingFile={setUploadingFile}
|
||||
alert={false}
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
@ -231,6 +231,7 @@ const styles = StyleSheet.create({
|
||||
profileData: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
paddingBottom: 16
|
||||
},
|
||||
})
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user