implementing blocked card restore

This commit is contained in:
balzack 2024-12-23 22:55:10 -08:00
parent 90e153fdd1
commit 9e23a2689d
13 changed files with 188 additions and 49 deletions

View File

@ -3,7 +3,7 @@ import { Focus } from 'databag-client-sdk'
import classes from './Conversation.module.css'
import { useConversation } from './useConversation.hook';
import { IconSend, IconTextSize, IconTextColor, IconVideo, IconFile, IconDisc, IconCamera, IconX, IconSettings, IconHome, IconServer, IconShield, IconLock, IconExclamationCircle } from '@tabler/icons-react'
import { Menu, Divider, Text, Textarea, ActionIcon, Loader } from '@mantine/core'
import { CloseButton, Menu, Divider, Text, Textarea, ActionIcon, Loader } from '@mantine/core'
import { Message } from '../message/Message';
import { modals } from '@mantine/modals'
import { ImageFile } from './imageFile/ImageFile';
@ -187,7 +187,7 @@ export function Conversation({ openDetails }: { openDetails: ()=>void }) {
)}
</div>
<div className={classes.control}>
<IconX size={24} className={classes.close} onClick={actions.close} />
<CloseButton className={classes.close} onClick={actions.close} />
</div>
</div>
<div ref={thread} className={classes.frame} style={state.loadingMore ? { overflow: 'hidden' } : { overflow: 'auto' }} onScroll={onScroll}>

View File

@ -5,6 +5,7 @@ import { IconUserCog, IconEyeOff, IconAlertHexagon, IconMessageX, IconLogout2, I
import { Switch, Button, Modal, Divider, Text, Textarea, Image, TextInput, ActionIcon } from '@mantine/core'
import { Card } from '../card/Card';
import { modals } from '@mantine/modals'
import { useDisclosure } from '@mantine/hooks'
export function Details({ showClose, close }: { showClose: boolean, close: () => void }) {
const { state, actions } = useDetails()
@ -12,7 +13,7 @@ export function Details({ showClose, close }: { showClose: boolean, close: () =>
const [removing, setRemoving] = useState(false);
const [blocking, setBlocking] = useState(false);
const [reporting, setReporting] = useState(false);
const [showModal, setShowModal] = useState(false);
const [showModal, { open: setShowModal, close: clearShowModal }] = useDisclosure(false)
const undo = () => {
actions.undoSubject();
@ -291,7 +292,7 @@ export function Details({ showClose, close }: { showClose: boolean, close: () =>
<Text className={classes.actionLabel}>{state.strings.remove}</Text>
</div>
<div className={classes.action}>
<ActionIcon variant="subtle" size={32} onClick={()=>setShowModal(true)}>
<ActionIcon variant="subtle" size={32} onClick={setShowModal}>
<IconUserCog size={32} />
</ActionIcon>
<Text className={classes.actionLabel}>{state.strings.members}</Text>
@ -324,7 +325,7 @@ export function Details({ showClose, close }: { showClose: boolean, close: () =>
<Text>{ state.strings.syncError }</Text>
</div>
)}
<Modal title={state.strings.editMembership} opened={showModal} onClose={() => setShowModal(false)} overlayProps={{ backgroundOpacity: 0.65, blur: 3 }} centered>
<Modal title={state.strings.editMembership} opened={showModal} onClose={clearShowModal} overlayProps={{ backgroundOpacity: 0.65, blur: 3 }} centered>
<div className={classes.modalContainer}>
{ members.length > 0 && (
<div className={classes.cardMembers}>
@ -337,7 +338,7 @@ export function Details({ showClose, close }: { showClose: boolean, close: () =>
</div>
)}
<div className={classes.controls}>
<Button variant="default" onClick={() => setShowModal(false)}>
<Button variant="default" onClick={clearShowModal}>
{state.strings.close}
</Button>
</div>

View File

@ -208,8 +208,12 @@ export function Message({ topic, card, profile, host }: { topic: Topic, card: Ca
{ (host || profile) && (
<IconTrash className={classes.careful} onClick={remove} />
)}
<IconForbid className={classes.careful} onClick={block} />
<IconFlag className={classes.careful} onClick={report} />
{ !profile && (
<IconForbid className={classes.careful} onClick={block} />
)}
{ !profile && (
<IconFlag className={classes.careful} onClick={report} />
)}
</div>
</div>
</div>

View File

@ -18,7 +18,7 @@ import {
IconCancel,
IconDeviceFloppy,
} from '@tabler/icons-react'
import { Text, Image, ActionIcon } from '@mantine/core'
import { CloseButton, Text, Image, ActionIcon } from '@mantine/core'
export type ProfileParams = {
guid: string
@ -239,9 +239,9 @@ export function Profile({ params, showClose, close }: { params: ProfileParams; s
return (
<div className={classes.contact}>
<div className={classes.header}>
{showClose && <IconX size={28} className={classes.match} />}
{showClose && <CloseButton className={classes.match} />}
<Text className={classes.label}>{`${state.handle}${state.node ? '/' + state.node : ''}`}</Text>
{showClose && <IconX size={30} className={classes.close} onClick={close} />}
{showClose && <CloseButton className={classes.close} onClick={close} />}
</div>
<div className={classes.detail}>
<div className={classes.image}>

View File

@ -278,3 +278,53 @@
}
}
}
.empty {
width: 600px;
height: 128px;
display: flex;
align-items: center;
justify-content: center;
border: 1px solid var(--mantine-color-text-7);
border-radius: 4px;
overflow: auto;
background: var(--mantine-color-surface-1);
.emptyLabel {
color: var(--mantine-color-text-6);
}
}
.blocked {
width: 600px;
min-height: 128px;
max-height: 256px;
border: 1px solid var(--mantine-color-text-7);
border-radius: 4px;
overflow: auto;
background: var(--mantine-color-surface-1);
.blockedItem {
display: flex;
align-items: center;
padding-left: 16px;
padding-right: 16px;
padding-top: 8px;
padding-bottom: 8px;
border-bottom: 1px solid var(--mantine-color-text-8);
.blockedValue {
flex-grow: 1;
font-size: 14px;
padding-right: 16px;
}
}
}
.controls {
display: flex;
width: 100%;
justify-content: flex-end;
padding-top: 8px;
}

View File

@ -1,5 +1,5 @@
import { useSettings } from './useSettings.hook'
import { Modal, Textarea, TextInput, PasswordInput, Radio, Group, Select, Switch, Text, PinInput, Image, Button, UnstyledButton } from '@mantine/core'
import { Modal, Textarea, TextInput, PasswordInput, ActionIcon, Radio, Group, Select, Switch, Text, PinInput, Image, Button, UnstyledButton } from '@mantine/core'
import classes from './Settings.module.css'
import {
IconLock,
@ -11,6 +11,7 @@ import {
IconVideo,
IconMicrophone,
IconWorld,
IconRestore,
IconBrightness,
IconTicket,
IconCloudLock,
@ -43,6 +44,9 @@ export function Settings({ showLogout }: { showLogout: boolean }) {
const [imageOpened, { open: imageOpen, close: imageClose }] = useDisclosure(false)
const [mfaOpened, { open: mfaOpen, close: mfaClose }] = useDisclosure(false)
const [sealOpened, { open: sealOpen, close: sealClose }] = useDisclosure(false)
const [blockedContactOpened, { open: blockedContactOpen, close: blockedContactClose }] = useDisclosure(false)
const [blockedTopicOpened, { open: blockedTopicOpen, close: blockedTopicClose }] = useDisclosure(false)
const [blockedMessageOpened, { open: blockedMessageOpen, close: blockedMessageClose }] = useDisclosure(false)
const [savingLogin, setSavingLogin] = useState(false)
const [savingDetails, setSavingDetails] = useState(false)
const [savingImage, setSavingImage] = useState(false)
@ -57,6 +61,27 @@ export function Settings({ showLogout }: { showLogout: boolean }) {
const [sealConfig, setSealConfig] = useState(false)
const [authMessage, setAuthMessage] = useState('')
const showBlockedCards = async () => {
await actions.loadBlockedCards();
blockedContactOpen();
}
const unblockCard = async (cardId: string) => {
try {
await actions.unblockCard(cardId);
} catch (err) {
console.log(err);
showError();
}
}
const blockedCards = state.blockedCards.map(blocked => (
<div className={classes.blockedItem}>
<Text className={classes.blockedValue}>CardID: { blocked.cardId }</Text>
<ActionIcon variant="subtle" size="md" onClick={()=>unblockCard(blocked.cardId)}><IconRestore /></ActionIcon>
</div>
));
const logout = () =>
modals.openConfirmModal({
title: state.strings.confirmLogout,
@ -430,19 +455,19 @@ export function Settings({ showLogout }: { showLogout: boolean }) {
</Text>
</div>
<div className={classes.divider} />
<div className={classes.entry}>
<div className={classes.entry} onClick={showBlockedCards}>
<div className={classes.entryIcon}>
<IconUserCancel />
</div>
<Text className={classes.entryLabel}>{state.strings.blockedContacts}</Text>
</div>
<div className={classes.entry}>
<div className={classes.entry} onClick={blockedTopicOpen}>
<div className={classes.entryIcon}>
<IconFolderCancel />
</div>
<Text className={classes.entryLabel}>{state.strings.blockedTopics}</Text>
</div>
<div className={classes.entry}>
<div className={classes.entry} onClick={blockedMessageOpen}>
<div className={classes.entryIcon}>
<IconMessage2Cancel />
</div>
@ -803,6 +828,29 @@ export function Settings({ showLogout }: { showLogout: boolean }) {
)}
</>
</Modal>
<Modal title={state.strings.blockedContacts} size="auto" opened={blockedContactOpened} onClose={blockedContactClose} overlayProps={{ backgroundOpacity: 0.55, blur: 3 }} centered>
{ blockedCards.length > 0 && (
<div className={classes.blocked}>
{ blockedCards }
</div>
)}
{ blockedCards.length === 0 && (
<div className={classes.empty}>
<Text className={classes.emptyLabel}>{ state.strings.noContacts }</Text>
</div>
)}
<div className={classes.controls}>
<Button variant="default" onClick={blockedContactClose}>
{state.strings.close}
</Button>
</div>
</Modal>
<Modal title={state.strings.blockedTopics} opened={blockedTopicOpened} onClose={blockedTopicClose} overlayProps={{ backgroundOpacity: 0.55, blur: 3 }} centered>
<div className={classes.blocked}></div>
</Modal>
<Modal title={state.strings.blockedMessages} opened={blockedMessageOpened} onClose={blockedMessageClose} overlayProps={{ backgroundOpacity: 0.55, blur: 3 }} centered>
<div className={classes.blocked}></div>
</Modal>
</>
)
}

View File

@ -50,6 +50,7 @@ export function useSettings() {
sealConfirm: '',
sealDelete: '',
secretCopied: false,
blockedCards: [] as {cardId: string}[],
})
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@ -112,6 +113,17 @@ export function useSettings() {
}, [display.state])
const actions = {
loadBlockedCards: async () => {
const settings = app.state.session.getSettings();
const blockedCards = await settings.getBlockedCards();
updateState({ blockedCards });
},
unblockCard: async (cardId: string) => {
const contact = app.state.session.getContact();
await contact.setBlockedCard(cardId, false);
const blockedCards = state.blockedCards.filter(blocked => blocked.cardId != cardId);
updateState({ blockedCards });
},
getUsernameStatus: async (username: string) => {
const { settings } = getSession()
return await settings.getUsernameStatus(username)

View File

@ -44,6 +44,10 @@ export interface Settings {
updateSeal(password: string): Promise<void>;
forgetSeal(): Promise<void>;
getBlockedCards(): Promise<{cardId: string}[]>;
getBlockedChannels(): Promise<{cardId: string | null, channelId: string}[]>;
getBlockedTopicis(): Promise<{cardId: string | null, channelId: string, topicId: string}[]>;
addConfigListener(ev: (config: Config) => void): void;
removeConfigListener(ev: (config: Config) => void): void;
}
@ -69,7 +73,6 @@ export interface Contact {
resyncCard(cardId: string): Promise<void>;
flagCard(cardId: string): Promise<void>;
setBlockedCard(cardId: string, blocked: boolean): Promise<void>;
getBlockedCards(): Promise<Card[]>;
getRegistry(handle: string | null, server: string | null): Promise<Profile[]>;
addCardListener(ev: (cards: Card[]) => void): void;
@ -91,7 +94,7 @@ export interface Content {
flagChannel(cardId: string | null, channelId: string): Promise<void>;
setBlockedChannel(cardId: string | null, channelId: string, blocked: boolean): Promise<void>;
getBlockedChannels(): Promise<Channel[]>;
clearBlockedChannelTopic(cardId: string | null, channelId: string, topicId: string): Promise<void>;
addChannelListener(ev: (arg: { channels: Channel[]; cardId: string | null }) => void): void;
removeChannelListener(ev: (arg: { channels: Channel[]; cardId: string | null }) => void): void;

View File

@ -199,7 +199,7 @@ export class ContactModule implements Contact {
this.blockedCard.add(cardId);
entry.card = this.setCard(cardId, entry.item);
this.emitCards();
await this.store.setMarker(this.guid, 'blocked_card', cardId, '');
await this.store.setMarker(this.guid, 'blocked_card', cardId, JSON.stringify({cardId}));
}
private async clearCardBlocked(cardId: string) {
@ -231,7 +231,7 @@ export class ContactModule implements Contact {
this.blockedCardChannel.add(id);
channelEntry.channel = this.setChannel(cardId, channelId, channelEntry.item);
this.emitChannels(cardId);
await this.store.setMarker(this.guid, 'blocked_card_channel', id, '');
await this.store.setMarker(this.guid, 'blocked_card_channel', id, JSON.stringify({ cardId, channelId }));
}
private async clearChannelBlocked(cardId: string, channelId: string) {
@ -841,30 +841,6 @@ export class ContactModule implements Contact {
}
}
public async getBlockedCards(): Promise<Card[]> {
return Array.from(this.cardEntries.entries())
.filter(([key, value]) => this.isCardBlocked(key))
.map(([key, value]) => value.card);
}
public async getBlockedChannels(): Promise<Channel[]> {
const channels: Channel[] = [];
this.channelEntries.forEach((card, cardId) => {
const cardChannels = Array.from(card.entries())
.filter(([key, value]) => this.isChannelBlocked(cardId, key))
.map(([key, value]) => value.channel);
cardChannels.forEach((channel) => {
channels.push(channel);
});
});
return channels;
}
public async getBlockedArticles(): Promise<Article[]> {
const articles: Article[] = [];
return articles;
}
public async setBlockedCard(cardId: string, blocked: boolean): Promise<void> {
const entry = this.cardEntries.get(cardId);
if (entry) {
@ -894,6 +870,14 @@ export class ContactModule implements Contact {
}
}
public async clearBlockedChannelTopic(cardId: string, channelId: string, topicId: string) {
const id = `${cardId}:${channelId}:${topicId}`
await this.store.clearMarker(guid, 'blocked_topic', id);
if (this.focus) {
await this.focus.clearBlockedChannelTopic(cardId, channelId, topicId);
}
}
public async setBlockedArticle(cardId: string, articleId: string, blocked: boolean): Promise<void> {
const entries = this.articleEntries.get(cardId);
if (entries) {

View File

@ -99,6 +99,13 @@ export class ContentModule implements Content {
return channels.concat(cardChannels);
}
public async clearBlockedChannelTopic(cardId: string | null, channelId: string, topicId: string): Promise<void> {
if (cardId) {
return await this.contact.clearBlockedChannelTopic(cardId, channelId, topicId);
}
return await this.stream.clearBlockedChannelTopic(channelId, topicId);
}
public addChannelListener(ev: (arg: { channels: Channel[]; cardId: string | null }) => void): void {
this.stream.addChannelListener(ev);
this.contact.addChannelListener(ev);

View File

@ -822,7 +822,7 @@ export class FocusModule implements Focus {
this.blocked.add(id);
entry.topic = this.setTopic(topicId, entry.item);
this.emitTopics();
await this.store.setMarker(guid, 'blocked_topic', id, '');
await this.store.setMarker(guid, 'blocked_topic', id, JSON.stringify({ cardId, channelId, topicId }));
}
}
@ -838,6 +838,12 @@ export class FocusModule implements Focus {
}
}
public async clearBlockedChannelTopic(cardId: string | null, channelId: string, topicId: string) {
if (cardId === this.cardId && channelId === this.channelId) {
await this.clearBlockTopic(topicId);
}
}
private isTopicBlocked(topicId: string): boolean {
const { cardId, channelId, guid } = this;
const id = `${cardId ? cardId : ''}:${channelId}:${topicId}`

View File

@ -279,4 +279,26 @@ export class SettingsModule implements Settings {
const { node, secure, token } = this;
await setAccountLogin(node, secure, token, username, password);
}
public async getBlockedCards(): Promise<{cardId: string}[]> {
const { guid } = this;
const blockedContacts = await this.store.getMarkers(guid, 'blocked_card');
return blockedContacts.map(marker => {
try {
return JSON.parse(marker.value);
} catch (err) {
return {};
}
});
}
public async getBlockedChannels(): Promise<{cardId: string | null, channelId: string}[]> {
const blockedChannels = await this.store.getMarkers(guid, 'blocked_card_channel');
return [];
}
public async getBlockedTopics(): Promise<{cardId: string | null, channelId: string, topicId: string}[]> {
const blockedTopics = await this.store.getMarkers(guid, 'blocked_topic');
return [];
}
}

View File

@ -383,10 +383,12 @@ export class StreamModule {
}
}
public async getBlockedChannels(): Promise<Channel[]> {
return Array.from(this.channelEntries.entries())
.filter(([key, value]) => this.isChannelBlocked(key))
.map(([key, value]) => value.channel);
public async clearBlockedChannelTopic(channelId: string, topicId: string) {
const id = `'':${channelId}:${topicId}`
await this.store.clearMarker(guid, 'blocked_topic', id);
if (this.focus) {
await this.focus.clearBlockedChannelTopic(null, channelId, topicId);
}
}
public async flagChannel(channelId: string): Promise<void> {
@ -507,7 +509,7 @@ export class StreamModule {
this.blocked.add(channelId);
entry.channel = this.setChannel(channelId, entry.item);
this.emitChannels();
await this.store.setMarker(this.guid, 'blocked_channel', channelId, '');
await this.store.setMarker(this.guid, 'blocked_channel', channelId, JSON.stringify({ cardId: null, channelId }));
}
private async clearChannelBlocked(channelId: string) {