NIP-65 Publish relay list

This commit is contained in:
KoalaSat 2023-02-16 23:32:02 +01:00
parent fd711c0c35
commit b6532b83e1
No known key found for this signature in database
GPG Key ID: 2F7F61C6146AB157
21 changed files with 307 additions and 121 deletions

View File

@ -29,5 +29,4 @@ jobs:
~/go/bin/noscl relay add "wss://nostr.ono.re" ~/go/bin/noscl relay add "wss://nostr.ono.re"
~/go/bin/noscl relay add "wss://relay.grunch.dev" ~/go/bin/noscl relay add "wss://relay.grunch.dev"
~/go/bin/noscl relay add "wss://nostr.developer.li" ~/go/bin/noscl relay add "wss://nostr.developer.li"
~/go/bin/noscl publish '${{ github.event.release.body }}' ~/go/bin/noscl publish "${{ github.event.release.body }}"
~/go/bin/noscl publish ${{ github.event.release.body }}

View File

@ -71,7 +71,9 @@ public class Event {
} else if (kind.equals("43")) { } else if (kind.equals("43")) {
hideGroupMessage(database); hideGroupMessage(database);
} else if (kind.equals("44")) { } else if (kind.equals("44")) {
blockUser(database); muteUser(database);
} else if (kind.equals("1002")) {
saveRelays(database);
} }
} catch (JSONException e) { } catch (JSONException e) {
e.printStackTrace(); e.printStackTrace();
@ -209,13 +211,14 @@ public class Event {
database.replace("nostros_notes", null, values); database.replace("nostros_notes", null, values);
} }
protected void blockUser(SQLiteDatabase database) throws JSONException { protected void muteUser(SQLiteDatabase database) throws JSONException {
JSONArray pTags = filterTags("p"); JSONArray pTags = filterTags("p");
String groupId = pTags.getJSONArray(0).getString(1); String groupId = pTags.getJSONArray(0).getString(1);
String query = "SELECT id FROM nostros_users WHERE id = ?"; String query = "SELECT id FROM nostros_users WHERE id = ?";
@SuppressLint("Recycle") Cursor cursor = database.rawQuery(query, new String[] {groupId}); @SuppressLint("Recycle") Cursor cursor = database.rawQuery(query, new String[] {groupId});
if (cursor.getCount() == 0) { if (cursor.getCount() == 0) {
} else {
ContentValues values = new ContentValues(); ContentValues values = new ContentValues();
values.put("muted_groups", 1); values.put("muted_groups", 1);
String whereClause = "id = ?"; String whereClause = "id = ?";
@ -409,7 +412,7 @@ public class Event {
values.put("valid_nip05", validateNip05(nip05) ? 1 : 0); values.put("valid_nip05", validateNip05(nip05) ? 1 : 0);
values.put("blocked", 0); values.put("blocked", 0);
database.insert("nostros_users", null, values); database.insert("nostros_users", null, values);
} else if (cursor.moveToFirst() && (cursor.isNull(0) || created_at > cursor.getInt(0))) { } else if (cursor.moveToFirst() && created_at > cursor.getInt(0)) {
if (cursor.getInt(1) == 0 || !cursor.getString(2).equals(nip05)) { if (cursor.getInt(1) == 0 || !cursor.getString(2).equals(nip05)) {
values.put("valid_nip05", validateNip05(nip05) ? 1 : 0); values.put("valid_nip05", validateNip05(nip05) ? 1 : 0);
} }
@ -422,23 +425,71 @@ public class Event {
} }
protected void savePets(SQLiteDatabase database) throws JSONException { protected void savePets(SQLiteDatabase database) throws JSONException {
String queryLast = "SELECT pet_at FROM nostros_users ORDER BY pet_at DESC LIMIT 1";
Cursor cursorLast = database.rawQuery(queryLast, new String[] {});
if (cursorLast.moveToFirst() && created_at > cursorLast.getInt(0)) {
ContentValues valuesIntial = new ContentValues();
valuesIntial.put("contact", 0);
String whereClauseInitial = "id = ?";
database.update("nostros_users", valuesIntial, whereClauseInitial, new String[]{});
for (int i = 0; i < tags.length(); ++i) {
JSONArray tag = tags.getJSONArray(i);
String petId = tag.getString(1);
String name = "";
if (tag.length() >= 4) {
name = tag.getString(3);
}
String query = "SELECT * FROM nostros_users WHERE id = ?";
Cursor cursor = database.rawQuery(query, new String[] {petId});
ContentValues values = new ContentValues();
values.put("pet_at", created_at);
values.put("contact", 1);
values.put("name", name);
values.put("blocked", 0);
if (cursor.getCount() == 0) {
values.put("id", petId);
database.insert("nostros_users", null, values);
} else {
String whereClause = "id = ?";
String[] whereArgs = new String[] {
petId
};
database.update("nostros_users", values, whereClause, whereArgs);
}
}
}
}
protected void saveRelays(SQLiteDatabase database) throws JSONException {
for (int i = 0; i < tags.length(); ++i) { for (int i = 0; i < tags.length(); ++i) {
JSONArray tag = tags.getJSONArray(i); JSONArray tag = tags.getJSONArray(i);
String petId = tag.getString(1); String url = tag.getString(1);
String name = ""; String mode = "";
if (tag.length() >= 4) { if (tag.length() > 1) {
name = tag.getString(3); mode = tag.getString(2);
} }
String query = "SELECT * FROM nostros_users WHERE id = ?";
Cursor cursor = database.rawQuery(query, new String[] {petId}); String query = "SELECT updated_at FROM nostros_relays WHERE url = ?";
Cursor cursor = database.rawQuery(query, new String[] {url});
ContentValues values = new ContentValues();
values.put("url", url);
values.put("mode", mode);
if (cursor.getCount() == 0) { if (cursor.getCount() == 0) {
ContentValues values = new ContentValues(); values.put("active", 0);
values.put("id", petId); values.put("global_feed", 0);
values.put("name", name); values.put("manual", 1);
values.put("contact", true); database.insert("nostros_relays", null, values);
values.put("blocked", 0); } else if (cursor.moveToFirst() && created_at > cursor.getInt(0)) {
values.put("pet_at", created_at); values.put("updated_at", created_at);
database.insert("nostros_users", null, values); String whereClause = "url = ?";
String[] whereArgs = new String[] {
url
};
database.update("nostros_relays", values, whereClause, whereArgs);
} }
} }
} }

View File

@ -173,6 +173,10 @@ public class DatabaseModule {
database.execSQL("ALTER TABLE nostros_group_messages ADD COLUMN read INT DEFAULT 0;"); database.execSQL("ALTER TABLE nostros_group_messages ADD COLUMN read INT DEFAULT 0;");
database.execSQL("ALTER TABLE nostros_group_messages ADD COLUMN user_mentioned INT DEFAULT 0;"); database.execSQL("ALTER TABLE nostros_group_messages ADD COLUMN user_mentioned INT DEFAULT 0;");
} catch (SQLException e) { } } catch (SQLException e) { }
try {
database.execSQL("ALTER TABLE updated_at ADD COLUMN mode INT DEFAULT 0;");
database.execSQL("ALTER TABLE nostros_relays ADD COLUMN mode TEXT;");
} catch (SQLException e) { }
} }
public void saveEvent(JSONObject data, String userPubKey, String relayUrl) throws JSONException { public void saveEvent(JSONObject data, String userPubKey, String relayUrl) throws JSONException {

View File

@ -27,6 +27,12 @@ export const MenuItems: React.FC = () => {
const { t } = useTranslation('common') const { t } = useTranslation('common')
const theme = useTheme() const theme = useTheme()
const [activerelays, setActiveRelays] = React.useState<number>(0)
React.useEffect(() => {
setActiveRelays(relays.filter((relay) => relay.active).length)
}, [relays])
const onPressLogout: () => void = () => { const onPressLogout: () => void = () => {
logout() logout()
} }
@ -110,12 +116,12 @@ export const MenuItems: React.FC = () => {
onPress={() => onPressItem('relays', 0)} onPress={() => onPressItem('relays', 0)}
onTouchEnd={() => setDrawerItemIndex(-1)} onTouchEnd={() => setDrawerItemIndex(-1)}
right={() => right={() =>
relays.length < 1 ? ( activerelays < 1 ? (
<Text style={{ color: theme.colors.error }}>{t('menuItems.notConnected')}</Text> <Text style={{ color: theme.colors.error }}>{t('menuItems.notConnected')}</Text>
) : ( ) : (
<Text style={{ color: theme.colors.inversePrimary }}> <Text style={{ color: '#7ADC70' }}>
{t('menuItems.connectedRelays', { {t('menuItems.connectedRelays', {
number: relays.filter((relay) => relay.active).length, number: activerelays,
})} })}
</Text> </Text>
) )

View File

@ -1,7 +1,7 @@
import { t } from 'i18next' import { t } from 'i18next'
import * as React from 'react' import * as React from 'react'
import { StyleSheet, View, ListRenderItem, Switch, FlatList } from 'react-native' import { StyleSheet, View, ListRenderItem, Switch, FlatList } from 'react-native'
import { IconButton, List, Snackbar, Text, useTheme } from 'react-native-paper' import { Button, IconButton, List, Snackbar, Text, useTheme } from 'react-native-paper'
import { AppContext } from '../../Contexts/AppContext' import { AppContext } from '../../Contexts/AppContext'
import { RelayPoolContext } from '../../Contexts/RelayPoolContext' import { RelayPoolContext } from '../../Contexts/RelayPoolContext'
import { UserContext } from '../../Contexts/UserContext' import { UserContext } from '../../Contexts/UserContext'
@ -10,6 +10,7 @@ import {
getUser, getUser,
updateUserBlock, updateUserBlock,
updateUserContact, updateUserContact,
updateUserMutesGroups,
User, User,
} from '../../Functions/DatabaseFunctions/Users' } from '../../Functions/DatabaseFunctions/Users'
import { populatePets, username } from '../../Functions/RelayFunctions/Users' import { populatePets, username } from '../../Functions/RelayFunctions/Users'
@ -22,6 +23,8 @@ import { relayToColor } from '../../Functions/NativeFunctions'
import { Relay } from '../../Functions/DatabaseFunctions/Relays' import { Relay } from '../../Functions/DatabaseFunctions/Relays'
import ProfileShare from '../ProfileShare' import ProfileShare from '../ProfileShare'
import { ScrollView } from 'react-native-gesture-handler' import { ScrollView } from 'react-native-gesture-handler'
import { Kind } from 'nostr-tools'
import { getUnixTime } from 'date-fns'
interface ProfileActionsProps { interface ProfileActionsProps {
user: User user: User
@ -40,10 +43,12 @@ export const ProfileActions: React.FC<ProfileActionsProps> = ({
const { relayPool, updateRelayItem } = React.useContext(RelayPoolContext) const { relayPool, updateRelayItem } = React.useContext(RelayPoolContext)
const [isContact, setIsContact] = React.useState<boolean>() const [isContact, setIsContact] = React.useState<boolean>()
const [isBlocked, setIsBlocked] = React.useState<boolean>() const [isBlocked, setIsBlocked] = React.useState<boolean>()
const [isMuted, setIsMuted] = React.useState<boolean>()
const [showNotification, setShowNotification] = React.useState<undefined | string>() const [showNotification, setShowNotification] = React.useState<undefined | string>()
const [showNotificationRelay, setShowNotificationRelay] = React.useState<undefined | string>() const [showNotificationRelay, setShowNotificationRelay] = React.useState<undefined | string>()
const bottomSheetRelaysRef = React.useRef<RBSheet>(null) const bottomSheetRelaysRef = React.useRef<RBSheet>(null)
const bottomSheetShareRef = React.useRef<RBSheet>(null) const bottomSheetShareRef = React.useRef<RBSheet>(null)
const bottomSheetMuteRef = React.useRef<RBSheet>(null)
const [userRelays, setUserRelays] = React.useState<NoteRelay[]>([]) const [userRelays, setUserRelays] = React.useState<NoteRelay[]>([])
const [openLn, setOpenLn] = React.useState<boolean>(false) const [openLn, setOpenLn] = React.useState<boolean>(false)
@ -52,6 +57,27 @@ export const ProfileActions: React.FC<ProfileActionsProps> = ({
loadRelays() loadRelays()
}, []) }, [])
const muteUser: () => void = () => {
if (publicKey && relayPool && database && user.id) {
relayPool
?.sendEvent({
content: '',
created_at: getUnixTime(new Date()),
kind: Kind.ChannelMuteUser,
pubkey: publicKey,
tags: [['p', user.id]],
})
.then(() => {
if (database) {
updateUserMutesGroups(database, user.id, true).then(() => {
setIsMuted(true)
bottomSheetMuteRef.current?.close()
})
}
})
}
}
const loadRelays: () => void = () => { const loadRelays: () => void = () => {
if (database) { if (database) {
getUserRelays(database, user.id).then((results) => { getUserRelays(database, user.id).then((results) => {
@ -69,6 +95,7 @@ export const ProfileActions: React.FC<ProfileActionsProps> = ({
setUser(result) setUser(result)
setIsContact(result.contact) setIsContact(result.contact)
setIsBlocked(result.blocked !== undefined && result.blocked > 0) setIsBlocked(result.blocked !== undefined && result.blocked > 0)
setIsMuted(result.muted_groups !== undefined && result.muted_groups > 0)
} else if (user.id === publicKey) { } else if (user.id === publicKey) {
setUser({ setUser({
id: publicKey, id: publicKey,
@ -203,12 +230,13 @@ export const ProfileActions: React.FC<ProfileActionsProps> = ({
<View style={styles.mainLayout}> <View style={styles.mainLayout}>
<View style={styles.actionButton}> <View style={styles.actionButton}>
<IconButton <IconButton
icon={user?.blocked && user?.blocked > 0 ? 'account-cancel' : 'account-cancel-outline'} icon='account-cancel-outline'
iconColor={isBlocked ? theme.colors.error : theme.colors.onSecondaryContainer}
size={28} size={28}
onPress={onChangeBlockUser} onPress={onChangeBlockUser}
/> />
<Text> <Text style={isBlocked ? { color: theme.colors.error } : {}}>
{t(user?.blocked && user?.blocked > 0 ? 'profileCard.unblock' : 'profileCard.block')} {t(isBlocked ? 'profileCard.blocked' : 'profileCard.block')}
</Text> </Text>
</View> </View>
<View style={styles.actionButton}> <View style={styles.actionButton}>
@ -228,6 +256,20 @@ export const ProfileActions: React.FC<ProfileActionsProps> = ({
<Text>{t('profileCard.relaysTitle')}</Text> <Text>{t('profileCard.relaysTitle')}</Text>
</View> </View>
</View> </View>
<View style={styles.mainLayout}>
<View style={styles.actionButton}>
<IconButton
icon={isMuted ? 'volume-off' : 'volume-high'}
iconColor={isMuted ? theme.colors.error : theme.colors.onSecondaryContainer}
size={28}
onPress={() => !isMuted && bottomSheetMuteRef.current?.open()}
disabled={user.id === publicKey}
/>
<Text style={isMuted ? { color: theme.colors.error } : {}}>
{t(isMuted ? 'profileCard.muted' : 'profileCard.mute')}
</Text>
</View>
</View>
<RBSheet ref={bottomSheetRelaysRef} closeOnDragDown={true} customStyles={bottomSheetStyles}> <RBSheet ref={bottomSheetRelaysRef} closeOnDragDown={true} customStyles={bottomSheetStyles}>
<View> <View>
<Text variant='titleLarge'>{t('profileCard.relaysTitle')}</Text> <Text variant='titleLarge'>{t('profileCard.relaysTitle')}</Text>
@ -265,6 +307,30 @@ export const ProfileActions: React.FC<ProfileActionsProps> = ({
<RBSheet ref={bottomSheetShareRef} closeOnDragDown={true} customStyles={bottomSheetStyles}> <RBSheet ref={bottomSheetShareRef} closeOnDragDown={true} customStyles={bottomSheetStyles}>
<ProfileShare user={user} /> <ProfileShare user={user} />
</RBSheet> </RBSheet>
<RBSheet ref={bottomSheetMuteRef} closeOnDragDown={true} customStyles={bottomSheetStyles}>
<View style={styles.muteContainer}>
<Text variant='titleLarge'>
{t('profileCard.muteUser', { username: username(user) })}
</Text>
<View style={[styles.warning, { backgroundColor: '#683D00' }]}>
<Text variant='titleSmall' style={[styles.warningTitle, { color: '#FFDCBB' }]}>
{t('profileCard.muteWarningTitle')}
</Text>
<Text style={{ color: '#FFDCBB' }}>{t('profileCard.muteWarning')}</Text>
</View>
<Button style={styles.buttonSpacer} mode='contained' onPress={muteUser}>
{t('profileCard.muteForever', { username: username(user) })}
</Button>
<Button
mode='outlined'
onPress={() => {
bottomSheetMuteRef.current?.close()
}}
>
{t('profileCard.cancel')}
</Button>
</View>
</RBSheet>
<LnPayment setOpen={setOpenLn} open={openLn} user={user} /> <LnPayment setOpen={setOpenLn} open={openLn} user={user} />
{showNotification && ( {showNotification && (
<Snackbar <Snackbar
@ -307,6 +373,22 @@ const styles = StyleSheet.create({
paddingLeft: 16, paddingLeft: 16,
textAlign: 'center', textAlign: 'center',
}, },
warning: {
borderRadius: 4,
padding: 16,
marginTop: 16,
marginBottom: 16,
},
warningTitle: {
marginBottom: 8,
},
buttonSpacer: {
marginTop: 16,
marginBottom: 16,
},
muteContainer: {
paddingRight: 16,
},
}) })
export default ProfileActions export default ProfileActions

View File

@ -6,6 +6,8 @@ import debounce from 'lodash.debounce'
import { getActiveRelays, getRelays, Relay } from '../Functions/DatabaseFunctions/Relays' import { getActiveRelays, getRelays, Relay } from '../Functions/DatabaseFunctions/Relays'
import { UserContext } from './UserContext' import { UserContext } from './UserContext'
import { randomInt } from '../Functions/NativeFunctions' import { randomInt } from '../Functions/NativeFunctions'
import { getUnixTime } from 'date-fns'
import { Event } from '../../lib/nostr/Events'
export interface RelayPoolContextProps { export interface RelayPoolContextProps {
relayPoolReady: boolean relayPoolReady: boolean
@ -54,6 +56,19 @@ export const RelayPoolContextProvider = ({
const [relays, setRelays] = React.useState<Relay[]>([]) const [relays, setRelays] = React.useState<Relay[]>([])
const [displayRelayDrawer, setDisplayrelayDrawer] = React.useState<string>() const [displayRelayDrawer, setDisplayrelayDrawer] = React.useState<string>()
const sendRelays: (relayList: Relay[]) => void = (relayList) => {
if (publicKey && relayList.length > 0) {
const event: Event = {
content: '',
created_at: getUnixTime(new Date()),
kind: 1002,
pubkey: publicKey,
tags: relayList.map((relay) => ['r', relay.url, relay.mode ?? '']),
}
relayPool?.sendEvent(event)
}
}
const changeEventIdHandler: (event: WebsocketEvent) => void = (event) => { const changeEventIdHandler: (event: WebsocketEvent) => void = (event) => {
setLastEventId(event.eventId) setLastEventId(event.eventId)
} }
@ -92,15 +107,15 @@ export const RelayPoolContextProvider = ({
} }
} }
const loadRelays: () => Promise<void> = async () => { const loadRelays: () => Promise<Relay[]> = async () => {
return await new Promise<void>((resolve, _reject) => { return await new Promise<Relay[]>((resolve, _reject) => {
if (database) { if (database) {
getRelays(database).then((results) => { getRelays(database).then((results) => {
setRelays(results) setRelays(results)
resolve() resolve(results)
}) })
} else { } else {
resolve() resolve([])
} }
}) })
} }
@ -118,7 +133,7 @@ export const RelayPoolContextProvider = ({
return await new Promise((resolve, _reject) => { return await new Promise((resolve, _reject) => {
if (relayPool && database && publicKey) { if (relayPool && database && publicKey) {
relayPool.update(relay.url, relay.active ?? 1, relay.global_feed ?? 1, () => { relayPool.update(relay.url, relay.active ?? 1, relay.global_feed ?? 1, () => {
loadRelays().then(resolve) loadRelays().then(() => resolve())
}) })
} }
}) })
@ -129,7 +144,10 @@ export const RelayPoolContextProvider = ({
return await new Promise((resolve, _reject) => { return await new Promise((resolve, _reject) => {
if (relayPool && database && publicKey) { if (relayPool && database && publicKey) {
relayPool.add(relay.url, () => { relayPool.add(relay.url, () => {
loadRelays().then(resolve) loadRelays().then((results) => {
sendRelays(results)
resolve()
})
}) })
} }
}) })
@ -140,7 +158,10 @@ export const RelayPoolContextProvider = ({
return await new Promise((resolve, _reject) => { return await new Promise((resolve, _reject) => {
if (relayPool && database && publicKey) { if (relayPool && database && publicKey) {
relayPool.remove(relay.url, () => { relayPool.remove(relay.url, () => {
loadRelays().then(resolve) loadRelays().then((results) => {
sendRelays(results)
resolve()
})
}) })
} }
}) })

View File

@ -37,9 +37,9 @@ export const updateConversationRead: (
return db.execute(userQuery, [1, conversationId]) return db.execute(userQuery, [1, conversationId])
} }
export const updateAllRead: (db: QuickSQLiteConnection) => Promise<QueryResult | null> = async ( export const updateAllDirectMessagesRead: (
db, db: QuickSQLiteConnection,
) => { ) => Promise<QueryResult | null> = async (db) => {
const userQuery = `UPDATE nostros_direct_messages SET read = ?` const userQuery = `UPDATE nostros_direct_messages SET read = ?`
return db.execute(userQuery, [1]) return db.execute(userQuery, [1])
} }

View File

@ -146,6 +146,13 @@ export const deleteGroup: (
return db.execute(userQuery, [groupId]) return db.execute(userQuery, [groupId])
} }
export const updateAllDirectMessagesRead: (
db: QuickSQLiteConnection,
) => Promise<QueryResult | null> = async (db) => {
const userQuery = `UPDATE nostros_group_messages SET read = ?`
return db.execute(userQuery, [1])
}
export const activateGroup: ( export const activateGroup: (
db: QuickSQLiteConnection, db: QuickSQLiteConnection,
groupId: string, groupId: string,

View File

@ -10,6 +10,7 @@ export interface Relay {
global_feed?: number global_feed?: number
resilient?: number resilient?: number
manual?: number manual?: number
mode?: 'read' | 'write' | ''
} }
export interface RelayInfo { export interface RelayInfo {

View File

@ -14,6 +14,7 @@ export interface User {
created_at?: number created_at?: number
valid_nip05?: boolean valid_nip05?: boolean
blocked?: number blocked?: number
muted_groups?: number
} }
const databaseToEntity: (object: object) => User = (object) => { const databaseToEntity: (object: object) => User = (object) => {
@ -42,6 +43,17 @@ export const updateUserBlock: (
return db.execute(userQuery, [blocked ? 1 : 0, userId]) return db.execute(userQuery, [blocked ? 1 : 0, userId])
} }
export const updateUserMutesGroups: (
db: QuickSQLiteConnection,
userId: string,
muted: boolean,
) => Promise<QueryResult | null> = async (db, userId, muted) => {
const userQuery = `UPDATE nostros_users SET muted_groups = ? WHERE id = ?`
await addUser(userId, db)
return db.execute(userQuery, [muted ? 1 : 0, userId])
}
export const getUser: (pubkey: string, db: QuickSQLiteConnection) => Promise<User | null> = async ( export const getUser: (pubkey: string, db: QuickSQLiteConnection) => Promise<User | null> = async (
pubkey, pubkey,
db, db,

View File

@ -370,7 +370,10 @@
"share": "Teilen", "share": "Teilen",
"unblock": "Erlauben", "unblock": "Erlauben",
"unfollow": "Abo entfernen", "unfollow": "Abo entfernen",
"relaysTitle": "Relays" "relaysTitle": "Relays",
"blocked": "Blocked",
"mute": "Mute",
"muted": "Muted"
}, },
"conversationsFeed": { "conversationsFeed": {
"openMessage": "Unterhaltung beginnen", "openMessage": "Unterhaltung beginnen",

View File

@ -313,7 +313,8 @@
}, },
"groupPage": { "groupPage": {
"typeMessage": "Type message", "typeMessage": "Type message",
"replyText": "Message" "replyText": "Message",
"admin": "ADMIN"
}, },
"groupHeaderIcon": { "groupHeaderIcon": {
"delete": "Leave group", "delete": "Leave group",
@ -368,7 +369,15 @@
"block": "Block", "block": "Block",
"unblock": "Unblock", "unblock": "Unblock",
"share": "Share", "share": "Share",
"relaysTitle": "Relays" "relaysTitle": "Relays",
"blocked": "Blocked",
"mute": "Mute",
"muted": "Muted",
"muteUser": "Mute {{username}} on chats",
"muteWarningTitle": "Important",
"muteWarning": "This action will hide the messages of this user on all chats and cannot be undone.",
"muteForever": "Mute {{username}} forever",
"cancel": "Cancel"
}, },
"conversationsFeed": { "conversationsFeed": {
"openMessage": "Start conversation", "openMessage": "Start conversation",

View File

@ -352,7 +352,10 @@
"share": "Share", "share": "Share",
"unblock": "Desbloquear", "unblock": "Desbloquear",
"unfollow": "Siguiendo", "unfollow": "Siguiendo",
"relaysTitle": "Relays" "relaysTitle": "Relays",
"blocked": "Bloqueado",
"mute": "Silenciar",
"muted": "Silenciado"
}, },
"conversationsFeed": { "conversationsFeed": {
"openMessage": "Comenzar conversación", "openMessage": "Comenzar conversación",

View File

@ -336,7 +336,10 @@
"block": "Bloquer", "block": "Bloquer",
"unblock": "Débloquer", "unblock": "Débloquer",
"unfollow": "Abonné", "unfollow": "Abonné",
"relaysTitle": "Relays" "relaysTitle": "Relays",
"blocked": "Blocked",
"mute": "Mute",
"muted": "Muted"
}, },
"conversationsFeed": { "conversationsFeed": {
"openMessage": "Commencer la conversation", "openMessage": "Commencer la conversation",

View File

@ -344,7 +344,10 @@
"unblock": "Desbloquear", "unblock": "Desbloquear",
"unfollow": "Following", "unfollow": "Following",
"share": "Share", "share": "Share",
"relaysTitle": "Relays" "relaysTitle": "Relays",
"blocked": "Blocked",
"mute": "Mute",
"muted": "Muted"
}, },
"conversationsFeed": { "conversationsFeed": {
"openMessage": "Start conversation", "openMessage": "Start conversation",

View File

@ -350,7 +350,10 @@
"share": "分享", "share": "分享",
"copyNip05": "复制 NIP-05", "copyNip05": "复制 NIP-05",
"copyNPub": "复制公钥", "copyNPub": "复制公钥",
"relaysTitle": "中继" "relaysTitle": "中继",
"blocked": "Blocked",
"mute": "Mute",
"muted": "Muted"
}, },
"conversationsFeed": { "conversationsFeed": {
"openMessage": "发送私信", "openMessage": "发送私信",

View File

@ -18,7 +18,7 @@ import ConfigPage from '../ConfigPage'
import { RelayPoolContext } from '../../Contexts/RelayPoolContext' import { RelayPoolContext } from '../../Contexts/RelayPoolContext'
import { AppContext } from '../../Contexts/AppContext' import { AppContext } from '../../Contexts/AppContext'
import RelayCard from '../../Components/RelayCard' import RelayCard from '../../Components/RelayCard'
import { updateAllRead } from '../../Functions/DatabaseFunctions/DirectMessages' import { updateAllDirectMessagesRead } from '../../Functions/DatabaseFunctions/DirectMessages'
import { getUnixTime } from 'date-fns' import { getUnixTime } from 'date-fns'
import ContactsPage from '../ContactsPage' import ContactsPage from '../ContactsPage'
import GroupPage from '../GroupPage' import GroupPage from '../GroupPage'
@ -60,8 +60,13 @@ export const HomeNavigator: React.FC = () => {
bottomSheetRef.current?.open() bottomSheetRef.current?.open()
} }
const onPressCheckAll: () => void = () => { const onMesssagesPressCheckAll: () => void = () => {
if (database) updateAllRead(database) if (database) updateAllDirectMessagesRead(database)
setRefreshBottomBarAt(getUnixTime(new Date()))
}
const onGroupsPressCheckAll: () => void = () => {
if (database) updateAllDirectMessagesRead(database)
setRefreshBottomBarAt(getUnixTime(new Date())) setRefreshBottomBarAt(getUnixTime(new Date()))
} }
@ -119,7 +124,18 @@ export const HomeNavigator: React.FC = () => {
/> />
)} )}
{['Landing'].includes(route.name) && historyKey?.includes('messages-') && ( {['Landing'].includes(route.name) && historyKey?.includes('messages-') && (
<Appbar.Action icon='check-all' isLeading onPress={() => onPressCheckAll()} /> <Appbar.Action
icon='check-all'
isLeading
onPress={() => onMesssagesPressCheckAll()}
/>
)}
{['Landing'].includes(route.name) && historyKey?.includes('groups-') && (
<Appbar.Action
icon='check-all'
isLeading
onPress={() => onGroupsPressCheckAll()}
/>
)} )}
{['Group'].includes(route.name) && ( {['Group'].includes(route.name) && (
<GroupHeaderIcon groupId={route.params?.groupId} /> <GroupHeaderIcon groupId={route.params?.groupId} />

View File

@ -32,7 +32,9 @@ import { handleInfinityScroll } from '../../Functions/NativeFunctions'
import NostrosAvatar from '../../Components/NostrosAvatar' import NostrosAvatar from '../../Components/NostrosAvatar'
import UploadImage from '../../Components/UploadImage' import UploadImage from '../../Components/UploadImage'
import { import {
getGroup,
getGroupMessages, getGroupMessages,
Group,
GroupMessage, GroupMessage,
updateGroupRead, updateGroupRead,
} from '../../Functions/DatabaseFunctions/Groups' } from '../../Functions/DatabaseFunctions/Groups'
@ -53,6 +55,7 @@ export const GroupPage: React.FC<GroupPageProps> = ({ route }) => {
const { relayPool, lastEventId } = useContext(RelayPoolContext) const { relayPool, lastEventId } = useContext(RelayPoolContext)
const { publicKey, privateKey, name, picture, validNip05 } = useContext(UserContext) const { publicKey, privateKey, name, picture, validNip05 } = useContext(UserContext)
const [pageSize, setPageSize] = useState<number>(initialPageSize) const [pageSize, setPageSize] = useState<number>(initialPageSize)
const [group, setGroup] = useState<Group>()
const [groupMessages, setGroupMessages] = useState<GroupMessage[]>([]) const [groupMessages, setGroupMessages] = useState<GroupMessage[]>([])
const [sendingMessages, setSendingMessages] = useState<GroupMessage[]>([]) const [sendingMessages, setSendingMessages] = useState<GroupMessage[]>([])
const [reply, setReply] = useState<GroupMessage>() const [reply, setReply] = useState<GroupMessage>()
@ -82,6 +85,7 @@ export const GroupPage: React.FC<GroupPageProps> = ({ route }) => {
const loadGroupMessages: (subscribe: boolean) => void = (subscribe) => { const loadGroupMessages: (subscribe: boolean) => void = (subscribe) => {
if (database && publicKey && route.params.groupId) { if (database && publicKey && route.params.groupId) {
getGroup(database, route.params.groupId).then(setGroup)
updateGroupRead(database, route.params.groupId) updateGroupRead(database, route.params.groupId)
getGroupMessages(database, route.params.groupId, { getGroupMessages(database, route.params.groupId, {
order: 'DESC', order: 'DESC',
@ -294,6 +298,11 @@ export const GroupPage: React.FC<GroupPageProps> = ({ route }) => {
)} )}
</View> </View>
<View style={styles.cardContentDate}> <View style={styles.cardContentDate}>
{item.pubkey === group?.pubkey && (
<View style={[styles.warning, { backgroundColor: '#683D00' }]}>
<Text style={{ color: '#FFDCBB' }}>{t('groupPage.admin')}</Text>
</View>
)}
{message?.pending && ( {message?.pending && (
<View style={styles.cardContentPending}> <View style={styles.cardContentPending}>
<MaterialCommunityIcons <MaterialCommunityIcons
@ -589,6 +598,12 @@ const styles = StyleSheet.create({
flex: 1, flex: 1,
paddingLeft: 16, paddingLeft: 16,
}, },
warning: {
borderRadius: 4,
paddingLeft: 5,
paddingRight: 5,
marginRight: 5,
},
input: { input: {
flexDirection: 'column-reverse', flexDirection: 'column-reverse',
marginTop: 16, marginTop: 16,

View File

@ -96,11 +96,10 @@ export const GroupsFeed: React.FC = () => {
results.forEach((group) => { results.forEach((group) => {
if (group.id) { if (group.id) {
getGroupMessagesMentionsCount(database, group.id).then((count) => { getGroupMessagesMentionsCount(database, group.id).then((count) => {
if (count > 0) setNewMentions((prev) => {
setNewMentions((prev) => { if (group.id) prev[group.id] = count
if (group.id) prev[group.id] = count return prev
return prev })
})
}) })
getGroupMessagesCount(database, group.id).then((count) => { getGroupMessagesCount(database, group.id).then((count) => {
setNewMessage((prev) => { setNewMessage((prev) => {

View File

@ -58,7 +58,7 @@ export const ProfileLoadPage: React.FC = () => {
if (publicKey && relayPoolReady) { if (publicKey && relayPoolReady) {
relayPool?.subscribe('profile-load-meta', [ relayPool?.subscribe('profile-load-meta', [
{ {
kinds: [Kind.Contacts, Kind.Metadata], kinds: [Kind.Contacts, Kind.Metadata, Kind.ChannelCreation, Kind.ChannelMetadata, 1002],
authors: [publicKey], authors: [publicKey],
}, },
]) ])

View File

@ -2,7 +2,7 @@ import React, { useContext, useState } from 'react'
import { FlatList, ListRenderItem, ScrollView, StyleSheet, View } from 'react-native' import { FlatList, ListRenderItem, ScrollView, StyleSheet, View } from 'react-native'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { RelayPoolContext } from '../../Contexts/RelayPoolContext' import { RelayPoolContext } from '../../Contexts/RelayPoolContext'
import { getRelays, Relay } from '../../Functions/DatabaseFunctions/Relays' import { Relay } from '../../Functions/DatabaseFunctions/Relays'
import { REGEX_SOCKET_LINK } from '../../Constants/Relay' import { REGEX_SOCKET_LINK } from '../../Constants/Relay'
import { import {
List, List,
@ -20,48 +20,38 @@ import RBSheet from 'react-native-raw-bottom-sheet'
import { relayToColor } from '../../Functions/NativeFunctions' import { relayToColor } from '../../Functions/NativeFunctions'
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons' import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons'
import { useFocusEffect } from '@react-navigation/native' import { useFocusEffect } from '@react-navigation/native'
import { AppContext } from '../../Contexts/AppContext' import { UserContext } from '../../Contexts/UserContext'
export const defaultRelays = [
'wss://brb.io',
'wss://damus.io',
'wss://nostr-pub.wellorder.net',
'wss://nostr.swiss-enigma.ch',
'wss://nostr.onsats.org',
'wss://nostr-pub.semisol.dev',
'wss://nostr.openchain.fr',
'wss://relay.nostr.info',
'wss://nostr.oxtr.dev',
'wss://nostr.ono.re',
'wss://relay.grunch.dev',
'wss://nostr.developer.li',
]
export const RelaysPage: React.FC = () => { export const RelaysPage: React.FC = () => {
const defaultRelayInput = React.useMemo(() => 'wss://', []) const defaultRelayInput = React.useMemo(() => 'wss://', [])
const { updateRelayItem, addRelayItem, relayPool, setDisplayrelayDrawer } = const { updateRelayItem, addRelayItem, relayPool, setDisplayrelayDrawer, relays } =
useContext(RelayPoolContext) useContext(RelayPoolContext)
const { database } = useContext(AppContext) const { publicKey } = useContext(UserContext)
const { t } = useTranslation('common') const { t } = useTranslation('common')
const theme = useTheme() const theme = useTheme()
const bottomSheetAddRef = React.useRef<RBSheet>(null) const bottomSheetAddRef = React.useRef<RBSheet>(null)
const bottomSheetResilenseRef = React.useRef<RBSheet>(null) const bottomSheetResilenseRef = React.useRef<RBSheet>(null)
const [relays, setRelays] = React.useState<Relay[]>([])
const [addRelayInput, setAddRelayInput] = useState<string>(defaultRelayInput) const [addRelayInput, setAddRelayInput] = useState<string>(defaultRelayInput)
const [showNotification, setShowNotification] = useState<string>() const [showNotification, setShowNotification] = useState<string>()
useFocusEffect( useFocusEffect(
React.useCallback(() => { React.useCallback(() => {
relayPool?.unsubscribeAll() relayPool?.unsubscribeAll()
updateRelays() if (publicKey) {
relayPool?.subscribe('relays', [
{
kinds: [1002],
authors: [publicKey],
limit: 1,
},
])
}
return () => {} return () => relayPool?.unsubscribe(['relays'])
}, []), }, []),
) )
const updateRelays: () => void = () => { React.useEffect(() => {}, [relays])
if (database) getRelays(database).then(setRelays)
}
const addRelay: (url: string) => void = (url) => { const addRelay: (url: string) => void = (url) => {
if (!relayList.find((relay) => relay.url === url)) { if (!relayList.find((relay) => relay.url === url)) {
@ -70,7 +60,6 @@ export const RelaysPage: React.FC = () => {
active: 1, active: 1,
global_feed: 1, global_feed: 1,
}).then(() => { }).then(() => {
updateRelays()
setShowNotification('add') setShowNotification('add')
}) })
} }
@ -79,7 +68,6 @@ export const RelaysPage: React.FC = () => {
const activeRelay: (relay: Relay) => void = (relay) => { const activeRelay: (relay: Relay) => void = (relay) => {
relay.active = 1 relay.active = 1
updateRelayItem(relay).then(() => { updateRelayItem(relay).then(() => {
updateRelays()
setShowNotification('active') setShowNotification('active')
}) })
} }
@ -88,7 +76,6 @@ export const RelaysPage: React.FC = () => {
relay.active = 0 relay.active = 0
relay.global_feed = 0 relay.global_feed = 0
updateRelayItem(relay).then(() => { updateRelayItem(relay).then(() => {
updateRelays()
setShowNotification('desactive') setShowNotification('desactive')
}) })
} }
@ -97,7 +84,6 @@ export const RelaysPage: React.FC = () => {
relay.active = 1 relay.active = 1
relay.global_feed = 1 relay.global_feed = 1
updateRelayItem(relay).then(() => { updateRelayItem(relay).then(() => {
updateRelays()
setShowNotification('globalFeedActive') setShowNotification('globalFeedActive')
}) })
} }
@ -105,7 +91,6 @@ export const RelaysPage: React.FC = () => {
const desactiveGlobalFeedRelay: (relay: Relay) => void = (relay) => { const desactiveGlobalFeedRelay: (relay: Relay) => void = (relay) => {
relay.global_feed = 0 relay.global_feed = 0
updateRelayItem(relay).then(() => { updateRelayItem(relay).then(() => {
updateRelays()
setShowNotification('globalFeedActiveUnactive') setShowNotification('globalFeedActiveUnactive')
}) })
} }
@ -145,8 +130,6 @@ export const RelaysPage: React.FC = () => {
return 0 return 0
}) })
const myRelays = relayList.filter((relay) => !defaultRelays.includes(relay.url))
const renderItem: ListRenderItem<Relay> = ({ item, index }) => { const renderItem: ListRenderItem<Relay> = ({ item, index }) => {
return ( return (
<List.Item <List.Item
@ -200,7 +183,7 @@ export const RelaysPage: React.FC = () => {
right={() => ( right={() => (
<> <>
{type === 'centralized' && ( {type === 'centralized' && (
<Text style={[styles.smallRelay, { color: theme.colors.errorContainer }]}> <Text style={[styles.smallRelay, { color: theme.colors.error }]}>
{relayPool?.resilientAssignation.centralizedRelays[item] && {relayPool?.resilientAssignation.centralizedRelays[item] &&
t('relaysPage.centralized')} t('relaysPage.centralized')}
</Text> </Text>
@ -258,33 +241,9 @@ export const RelaysPage: React.FC = () => {
data={Object.keys(relayPool?.resilientAssignation.smallRelays ?? {})} data={Object.keys(relayPool?.resilientAssignation.smallRelays ?? {})}
renderItem={(data) => renderResilienceItem(data.item, data.index, 'small')} renderItem={(data) => renderResilienceItem(data.item, data.index, 'small')}
/> />
{myRelays.length > 0 && (
<>
<View style={styles.titleWrapper}>
<Text style={styles.title} variant='titleMedium'>
{t('relaysPage.myList')}
</Text>
<Divider />
</View>
<List.Item
title={t('relaysPage.relayName')}
right={() => (
<>
<Text style={styles.listHeader}>{t('relaysPage.globalFeed')}</Text>
<Text style={styles.listHeader}>{t('relaysPage.active')}</Text>
</>
)}
/>
<FlatList
showsVerticalScrollIndicator={false}
data={myRelays}
renderItem={renderItem}
/>
</>
)}
<View style={styles.titleWrapper}> <View style={styles.titleWrapper}>
<Text style={styles.title} variant='titleMedium'> <Text style={styles.title} variant='titleMedium'>
{t('relaysPage.recommended')} {t('relaysPage.myList')}
</Text> </Text>
<Divider /> <Divider />
</View> </View>
@ -297,17 +256,7 @@ export const RelaysPage: React.FC = () => {
</> </>
)} )}
/> />
<FlatList <FlatList showsVerticalScrollIndicator={false} data={relays} renderItem={renderItem} />
showsVerticalScrollIndicator={false}
data={defaultRelays.map(
(url) =>
relays.find((relay) => relay.url === url && relay.active && relay.active > 0) ?? {
url,
},
)}
renderItem={renderItem}
style={styles.list}
/>
</ScrollView> </ScrollView>
<AnimatedFAB <AnimatedFAB
style={styles.fab} style={styles.fab}