add topic modal for web app

This commit is contained in:
balzack 2024-11-13 09:38:49 -08:00
parent 20b42bf670
commit fef6c7b1dd
4 changed files with 212 additions and 117 deletions

View File

@ -78,7 +78,7 @@
padding-bottom: 8px; padding-bottom: 8px;
padding-right: 16px; padding-right: 16px;
padding-left: 16px; padding-left: 16px;
border-bottom: 1px solid var(--mantine-color-text-9); border-bottom: 1px solid var(--mantine-color-text-8);
&:hover { &:hover {
background: var(--mantine-color-surface-4); background: var(--mantine-color-surface-4);

View File

@ -15,7 +15,7 @@
padding-left: 16px; padding-left: 16px;
padding-right: 16px; padding-right: 16px;
padding-bottom: 8px; padding-bottom: 8px;
border-bottom: 2px solid var(--mantine-color-text-9); border-bottom: 2px solid var(--mantine-color-text-7);
width: 100%; width: 100%;
.input { .input {
@ -37,7 +37,7 @@
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
border-top: 2px solid var(--mantine-color-text-9); border-top: 2px solid var(--mantine-color-text-7);
.add { .add {
padding-left: 48px; padding-left: 48px;
@ -68,7 +68,7 @@
padding-bottom: 8px; padding-bottom: 8px;
padding-right: 16px; padding-right: 16px;
padding-left: 16px; padding-left: 16px;
border-bottom: 1px solid var(--mantine-color-text-9); border-bottom: 1px solid var(--mantine-color-text-8);
&:hover { &:hover {
background: var(--mantine-color-surface-4); background: var(--mantine-color-surface-4);
@ -76,3 +76,15 @@
} }
} }
} }
.addContainer {
display: flex;
flex-direction: column;
gap: 16px;
.addMembers {
width: 100%;
height: 200px;
background: var(--mantine-color-surface-0);
}
}

View File

@ -1,13 +1,17 @@
import React from 'react'; import React, {useState} from 'react';
import { useContent } from './useContent.hook' import { useContent } from './useContent.hook'
import { TextInput, Button } from '@mantine/core' import { Modal, TextInput, Button } from '@mantine/core'
import { IconSearch, IconMessagePlus } from '@tabler/icons-react' import { IconSearch, IconMessagePlus, IconLabel } from '@tabler/icons-react'
import classes from './Content.module.css' import classes from './Content.module.css'
import { Channel } from '../channel/Channel' import { Channel } from '../channel/Channel'
import { Focus } from 'databag-client-sdk' import { Focus } from 'databag-client-sdk'
export function Content({ select }: { select: (focus: Focus) => void }) { export function Content({ select }: { select: (focus: Focus) => void }) {
const { state, actions } = useContent() const { state, actions } = useContent()
const [add, setAdd] = useState(false);
const addTopic = async () => {
}
const channels = state.filtered.map((channel, idx) => { const channels = state.filtered.map((channel, idx) => {
return ( return (
@ -41,7 +45,7 @@ export function Content({ select }: { select: (focus: Focus) => void }) {
onChange={(event) => actions.setFilter(event.currentTarget.value)} onChange={(event) => actions.setFilter(event.currentTarget.value)}
/> />
{state.layout === 'small' && ( {state.layout === 'small' && (
<Button className={classes.add} leftSection={<IconMessagePlus size={20} />} onClick={actions.addChannel}> <Button className={classes.add} leftSection={<IconMessagePlus size={20} />} onClick={() => setAdd(true)}>
{state.strings.add} {state.strings.add}
</Button> </Button>
)} )}
@ -50,11 +54,27 @@ export function Content({ select }: { select: (focus: Focus) => void }) {
{channels.length !== 0 && <div className={classes.channels}>{channels}</div>} {channels.length !== 0 && <div className={classes.channels}>{channels}</div>}
{state.layout === 'large' && ( {state.layout === 'large' && (
<div className={classes.bar}> <div className={classes.bar}>
<Button className={classes.add} leftSection={<IconMessagePlus size={20} />} onClick={actions.addChannel}> <Button className={classes.add} leftSection={<IconMessagePlus size={20} />} onClick={() => setAdd(true)}>
{state.strings.add} {state.strings.add}
</Button> </Button>
</div> </div>
)} )}
<Modal title={state.strings.newTopic} opened={add} onClose={() => setAdd(false)} overlayProps={{ backgroundOpacity: 0.55, blur: 3 }} centered>
<div className={classes.addContainer}>
<TextInput
className={classes.input}
size="sm"
leftSectionPointerEvents="none"
leftSection={<IconLabel size={20} />}
placeholder={state.strings.subjectOptional}
value={state.filter}
onChange={(event) => actions.setFilter(event.currentTarget.value)}
/>
<div className={classes.addMembers}>
</div>
</div>
</Modal>
</div> </div>
) )
} }

View File

@ -1,180 +1,243 @@
import { useState, useContext, useEffect, useRef } from 'react' import {useState, useContext, useEffect, useRef} from 'react';
import { AppContext } from '../context/AppContext' import {AppContext} from '../context/AppContext';
import { DisplayContext } from '../context/DisplayContext' import {DisplayContext} from '../context/DisplayContext';
import { ContextType } from '../context/ContextType' import {ContextType} from '../context/ContextType';
import { Channel, Card, Profile } from 'databag-client-sdk' import {Channel, Card, Profile, Config} from 'databag-client-sdk';
import { notes, unknown, iii_group, iiii_group, iiiii_group, group } from '../constants/Icons' import {notes, unknown, iii_group, iiii_group, iiiii_group, group} from '../constants/Icons';
type ChannelParams = { type ChannelParams = {
cardId: string cardId: string;
channelId: string channelId: string;
sealed: boolean sealed: boolean;
hosted: boolean hosted: boolean;
unread: boolean unread: boolean;
imageUrl: string imageUrl: string;
subject: (string | null)[] subject: (string | null)[];
message: string message: string;
} };
export function useContent() { export function useContent() {
const cardChannels = useRef(new Map<string | null, Channel[]>()) const cardChannels = useRef(new Map<string | null, Channel[]>());
const app = useContext(AppContext) as ContextType const app = useContext(AppContext) as ContextType;
const display = useContext(DisplayContext) as ContextType const display = useContext(DisplayContext) as ContextType;
const [state, setState] = useState({ const [state, setState] = useState({
strings: display.state.strings, strings: display.state.strings,
layout: null, layout: null,
guid: '', guid: '',
cards: [] as Card[], connected: [] as Card[],
connectedAndSealable: [] as Cards[],
sorted: [] as Channel[], sorted: [] as Channel[],
filtered: [] as ChannelParams[], filtered: [] as ChannelParams[],
filter: '', filter: '',
}) topic: '',
sealable: false,
});
const compare = (a: Card, b: Card) => {
const aval = `${a.handle}/${a.node}`;
const bval = `${b.handle}/${b.node}`;
if (aval < bval) {
return state.sortAsc ? 1 : -1;
} else if (aval > bval) {
return state.sortAsc ? -1 : 1;
}
return 0;
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
const updateState = (value: any) => { const updateState = (value: any) => {
setState((s) => ({ ...s, ...value })) setState(s => ({...s, ...value}));
} };
useEffect(() => { useEffect(() => {
const { layout } = display.state const {layout} = display.state;
updateState({ layout }) updateState({layout});
}, [display.state]) }, [display.state]);
useEffect(() => { useEffect(() => {
const channels = state.sorted.map((channel) => { const channels = state.sorted.map(channel => {
const { cardId, channelId, unread, sealed, members, data, lastTopic } = channel const {cardId, channelId, unread, sealed, members, dataType, data, lastTopic} = channel;
const contacts = [] as (Card | undefined)[] const contacts = [] as (Card | undefined)[];
if (cardId) { if (cardId) {
const card = state.cards.find((contact) => contact.cardId === cardId) const card = state.cards.find(contact => contact.cardId === cardId);
if (card) { if (card) {
contacts.push(card) contacts.push(card);
} }
} }
const guests = members.filter((contact) => contact.guid !== state.guid) const guests = members.filter(contact => contact.guid !== state.guid);
const guestCards = guests const guestCards = guests
.map((contact) => state.cards.find((card) => card.guid === contact.guid)) .map(contact => state.cards.find(card => card.guid === contact.guid))
.sort((a, b) => { .sort((a, b) => {
if (!a && !b) { if (!a && !b) {
return 0 return 0;
} else if (!a) { } else if (!a) {
return 1 return 1;
} else if (!b) { } else if (!b) {
return -1 return -1;
} else if (a.handle > b.handle) { } else if (a.handle > b.handle) {
return 1 return 1;
} else { } else {
return 0 return 0;
} }
}) });
contacts.push(...guestCards) contacts.push(...guestCards);
const buildSubject = () => { const buildSubject = () => {
if (contacts.length === 0) { if (contacts.length === 0) {
return [] return [];
} }
return contacts.map((contact) => (contact ? contact.handle : null)) return contacts.map(contact => (contact ? contact.handle : null));
} };
const selectImage = () => { const selectImage = () => {
if (contacts.length == 0) { if (contacts.length == 0) {
return notes return notes;
} else if (contacts.length == 1) { } else if (contacts.length == 1) {
if (contacts[0]) { if (contacts[0]) {
return contacts[0].imageUrl return contacts[0].imageUrl;
} else { } else {
return unknown return unknown;
} }
} else if (contacts.length == 2) { } else if (contacts.length == 2) {
return iii_group return iii_group;
} else if (contacts.length == 3) { } else if (contacts.length == 3) {
return iiii_group return iiii_group;
} else if (contacts.length == 4) { } else if (contacts.length == 4) {
return iiiii_group return iiiii_group;
} else { } else {
return group return group;
}
};
const getMessage = () => {
if (!lastTopic || !lastTopic.status) {
return '';
}
if (lastTopic.dataType === 'superbasictopic') {
if (lastTopic.data?.text) {
return lastTopic.data.text;
} else {
return ''
}
} else if (lastTopic.dataType === 'sealedtopic') {
if (lastTopic.data) {
if (lastTopic.data.message?.text) {
return lastTopic.data.message.text;
} else {
return '';
}
} else {
return null;
}
} }
} }
const hosted = cardId == null const hosted = cardId == null;
const subject = data?.subject ? [data.subject] : buildSubject() const subject = data?.subject ? [data.subject] : buildSubject();
const message = lastTopic ? (lastTopic.data ? lastTopic.data.text : null) : '' const message = getMessage();
const imageUrl = selectImage() const imageUrl = selectImage();
return { cardId, channelId, sealed, hosted, unread, imageUrl, subject, message } return {cardId, channelId, sealed, hosted, unread, imageUrl, subject, message};
}) });
const search = state.filter?.toLowerCase() const search = state.filter?.toLowerCase();
const filtered = channels.filter((item) => { const filtered = channels.filter(item => {
if (search) { if (search) {
if (item.subject?.find((value) => value?.toLowerCase().includes(search))) { if (item.subject?.find(value => value?.toLowerCase().includes(search))) {
return true return true;
} }
if (item.message?.toLowerCase().includes(search)) { if (item.message?.toLowerCase().includes(search)) {
return true return true;
} }
return false return false;
} }
return true return true;
}) });
updateState({ filtered }) updateState({filtered});
}, [state.sorted, state.cards, state.guid, state.filter]) }, [state.sorted, state.cards, state.guid, state.filter]);
useEffect(() => { useEffect(() => {
const setConfig = (config: Config) => {
const { sealSet, sealUnlocked } = config;
updateState({ sealSet: sealSet && sealUnlocked });
};
const setProfile = (profile: Profile) => { const setProfile = (profile: Profile) => {
const { guid } = profile const {guid} = profile;
updateState({ guid }) updateState({guid});
} };
const setCards = (cards: Card[]) => { const setCards = (cards: Card[]) => {
updateState({ cards }) const sorted = cards.sort(compare);
} const connected = [];
const setChannels = ({ channels, cardId }: { channels: Channel[], cardId: string | null }) => { const sealable = [];
cardChannels.current.set(cardId, channels) sorted.forEach(card => {
const merged = [] as Channel[] if (card.status === 'connected') {
cardChannels.current.forEach((values) => { connected.push(card);
merged.push(...values) if (card.sealable) {
}) sealable.push(card);
const sorted = merged.sort((a, b) => { }
const aUpdated = a?.lastTopic?.created
const bUpdated = b?.lastTopic?.created
if (aUpdated == bUpdated) {
return 0
} else if (!aUpdated) {
return 1
} else if (!bUpdated) {
return -1
} else if (aUpdated < bUpdated) {
return 1
} else {
return -1
} }
}) });
updateState({ sorted }) updateState({cards, connected, sealable});
} };
const setChannels = ({channels, cardId}: {channels: Channel[]; cardId: string | null}) => {
cardChannels.current.set(cardId, channels);
const merged = [] as Channel[];
cardChannels.current.forEach(values => {
merged.push(...values);
});
const sorted = merged.sort((a, b) => {
const aUpdated = a?.lastTopic?.created;
const bUpdated = b?.lastTopic?.created;
if (aUpdated == bUpdated) {
return 0;
} else if (!aUpdated) {
return 1;
} else if (!bUpdated) {
return -1;
} else if (aUpdated < bUpdated) {
return 1;
} else {
return -1;
}
});
updateState({sorted});
};
const { identity, contact, content } = app.state.session const {identity, contact, content, settings} = app.state.session;
identity.addProfileListener(setProfile) identity.addProfileListener(setProfile);
contact.addCardListener(setCards) contact.addCardListener(setCards);
content.addChannelListener(setChannels) content.addChannelListener(setChannels);
settings.addConfigListener(setConfig);
return () => { return () => {
identity.removeProfileListener(setProfile) identity.removeProfileListener(setProfile);
contact.removeCardListener(setCards) contact.removeCardListener(setCards);
content.removeChannelListener(setChannels) content.removeChannelListener(setChannels);
} settings.removeConfigListener(setConfig);
}, []) };
}, []);
const actions = { const actions = {
addChannel: () => {
console.log('add channel');
},
setFilter: (filter: string) => { setFilter: (filter: string) => {
updateState({ filter }) updateState({filter});
},
setTopic: (topic: string) => {
updateState({topic});
}, },
getFocus: (cardId: string | null, channelId: string) => { getFocus: (cardId: string | null, channelId: string) => {
return app.state.session.setFocus(cardId, channelId) return app.state.session.setFocus(cardId, channelId);
}, },
} addTopic: async (sealed: boolean, subject: string, contacts: string[]) => {
const content = app.state.session.getContent();
await new Promise(r => setTimeout(r, 2000));
if (sealed) {
await content.addChannel(true, 'sealed', {subject}, contacts);
} else {
await content.addChannel(false, 'superbasic', {subject}, contacts);
}
},
};
return { state, actions } return {state, actions};
} }