show loading indicator as channels loading

This commit is contained in:
Roland Osborne 2025-02-28 14:49:44 -08:00
parent f120524531
commit b23598b364
9 changed files with 74 additions and 33 deletions

View File

@ -38,13 +38,17 @@
color: var(--mantine-color-text-6);
}
.steps {
display: flex;
align-items: center;
.step {
font-size: 16px;
color: var(--mantine-color-text-6);
.instructions {
height: 32px;
.steps {
display: flex;
align-items: center;
.step {
font-size: 16px;
color: var(--mantine-color-text-6);
}
}
}
}

View File

@ -16,19 +16,21 @@ export function Base() {
<Text className={classes.label}>{ state.strings.communication }</Text>
</div>
<Image className={classes.image} src={state.scheme === 'dark' ? dark : light} fit="contain" />
{ (state.profileSet === false || state.cardSet === false || state.channelSet === false) && (
<div className={classes.steps}>
{ (state.profileSet === false) && (
<Text className={classes.step}>{ state.strings.setupProfile }</Text>
)}
<IconChevronRight className={classes.icon} />
{ (state.profileSet === false || state.cardSet === false) && (
<Text className={classes.step}>{ state.strings.connectPeople }</Text>
)}
<IconChevronRight className={classes.icon} />
<Text className={classes.step}>{ state.strings.startConversation }</Text>
</div>
)}
<div className={classes.instructions}>
{ state.contentSet && (state.profileSet === false || state.cardSet === false || state.channelSet === false) && (
<div className={classes.steps}>
{ (state.profileSet === false) && (
<Text className={classes.step}>{ state.strings.setupProfile }</Text>
)}
<IconChevronRight className={classes.icon} />
{ (state.profileSet === false || state.cardSet === false) && (
<Text className={classes.step}>{ state.strings.connectPeople }</Text>
)}
<IconChevronRight className={classes.icon} />
<Text className={classes.step}>{ state.strings.startConversation }</Text>
</div>
)}
</div>
</div>
);
}

View File

@ -2,6 +2,7 @@ import { useState, useContext, useEffect } from 'react'
import { AppContext } from '../context/AppContext'
import { DisplayContext } from '../context/DisplayContext'
import { ContextType } from '../context/ContextType'
import { Card, Channel, Profile } from 'databag-client-sdk';
export function useBase() {
const app = useContext(AppContext) as ContextType
@ -12,6 +13,7 @@ export function useBase() {
profileSet: null as null | boolean,
cardSet: null as null | boolean,
channelSet: null as null | boolean,
contentSet: null as null | boolean,
})
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@ -34,16 +36,21 @@ export function useBase() {
const setChannels = ({ channels, cardId }: { channels: Channel[]; cardId: string | null }) => {
updateState({ channelSet: channels.length > 0 });
}
const setContent = (loaded: boolean) => {
updateState({ contentSet: loaded });
}
const { identity, contact, content } = app.state.session
identity.addProfileListener(setProfile)
contact.addCardListener(setCards)
content.addChannelListener(setChannels)
content.addLoadedListener(setContent);
return () => {
identity.removeProfileListener(setProfile);
contact.removeCardListener(setCards);
content.removeChannelListener(setChannels);
content.removeLoadedListener(setContent);
}
}, []);

View File

@ -1,11 +1,12 @@
import React, { useState, useEffect } from 'react'
import { useContent } from './useContent.hook'
import { Modal, Text, Switch, TextInput, Button } from '@mantine/core'
import { Modal, Box, LoadingOverlay, Text, Switch, TextInput, Button } from '@mantine/core'
import { IconSearch, IconMessagePlus, IconLabel } from '@tabler/icons-react'
import classes from './Content.module.css'
import { Channel } from '../channel/Channel'
import { Card } from '../card/Card'
import { modals } from '@mantine/modals'
import { Colors } from '../constants/Colors';
export function Content({ textCard }: { textCard: { cardId: null|string }}) {
const { state, actions } = useContent()
@ -122,8 +123,11 @@ export function Content({ textCard }: { textCard: { cardId: null|string }}) {
</Button>
)}
</div>
{channels.length === 0 && <div className={classes.none}>{state.strings.noTopics}</div>}
{channels.length !== 0 && <div className={classes.channels}>{channels}</div>}
<Box className={classes.channels} pos="relative" onClick={actions.setLoaded}>
{channels.length === 0 && <div className={classes.none}>{state.strings.noTopics}</div>}
{channels.length !== 0 && <div className={classes.channels}>{channels}</div>}
<LoadingOverlay visible={!state.loaded} zIndex={1000} overlayProps={{ radius: 'sm', blur: 1 }} loaderProps={{ color: Colors.primary, type: 'dots' }}/>
</Box>
{state.layout === 'large' && (
<div className={classes.bar}>
<Button className={classes.add} leftSection={<IconMessagePlus size={20} />} onClick={() => setAdd(true)}>

View File

@ -34,6 +34,7 @@ export function useContent() {
topic: '',
sealSet: false,
focused: null as null|{cardId: null|string, channelId: string},
loaded: null as null | boolean,
})
const compare = (a: Card, b: Card) => {
@ -180,6 +181,9 @@ export function useContent() {
const { sealSet, sealUnlocked } = config
updateState({ sealSet: sealSet && sealUnlocked })
}
const setLoaded = (loaded: boolean) => {
updateState({ loaded });
}
const setProfile = (profile: Profile) => {
const { guid } = profile
updateState({ guid })
@ -228,12 +232,14 @@ export function useContent() {
identity.addProfileListener(setProfile)
contact.addCardListener(setCards)
content.addChannelListener(setChannels)
content.addLoadedListener(setLoaded)
settings.addConfigListener(setConfig)
return () => {
identity.removeProfileListener(setProfile)
contact.removeCardListener(setCards)
content.removeChannelListener(setChannels)
content.removeLoadedListener(setLoaded)
settings.removeConfigListener(setConfig)
}
}, [])
@ -248,6 +254,9 @@ export function useContent() {
setFocus: async (cardId: string | null, channelId: string) => {
await app.actions.setFocus(cardId, channelId)
},
setLoaded: () => {
updateState({ loaded: true });
},
openTopic: async (cardId: string) => {
const content = app.state.session.getContent()
const card = state.cards.find(card => card.cardId === cardId)

View File

@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react';
import React, { useEffect, useState, useRef } from 'react';
import { useRing } from './useRing.hook';
import classes from './Ring.module.css';
import { Card as Contact } from '../card/Card';
@ -6,7 +6,7 @@ import { Card } from 'databag-client-sdk';
import { Colors } from '../constants/Colors';
import { modals } from '@mantine/modals'
import { Loader, Image, Text, ActionIcon } from '@mantine/core'
import { IconVideoPlus, IconEyeX, IconPhone, IconPhoneOff, IconArrowsMaximize, IconMicrophone, IconMicrophoneOff } from '@tabler/icons-react'
import { IconBell, IconVideoPlus, IconEyeX, IconPhone, IconPhoneOff, IconArrowsMaximize, IconMicrophone, IconMicrophoneOff } from '@tabler/icons-react'
export function Ring() {
const { state, actions } = useRing();
@ -15,6 +15,16 @@ export function Ring() {
const [accepting, setAccepting] = useState(null as null|string);
const [ignoring, setIgnoring] = useState(null as null|string);
const [declining, setDeclining] = useState(null as null|string);
const [ringing, setRinging] = useState(0);
const counter = useRef(0);
useEffect(() => {
const count = setInterval(() => {
counter.current += 1;
setRinging(counter.current);
}, 500);
return () => clearInterval(count);
}, []);
const showError = () => {
modals.openConfirmModal({
@ -154,7 +164,7 @@ export function Ring() {
<Text className={classes.duration}>{ `${Math.floor(state.duration/60)}:${(state.duration % 60).toString().padStart(2, '0')}` }</Text>
)}
{ !state.connected && (
<Loader size={18} />
<IconBell size={18} color={Colors.primary} style={{ rotate: ringing % 2 == 0 ? '15deg' : '-15deg' }} />
)}
</div>
<div className={classes.end}>

View File

@ -192,8 +192,6 @@ export class ContactModule implements Contact {
this.unsealAll = true;
this.syncing = false;
await this.sync();
this.loaded = true;
this.emitLoaded();
}
@ -554,6 +552,7 @@ export class ContactModule implements Contact {
this.emitCards();
await this.store.setContactRevision(guid, nextRev);
this.revision = nextRev;
this.emitLoaded();
if (this.nextRevision === nextRev) {
this.nextRevision = null;
}
@ -777,7 +776,10 @@ export class ContactModule implements Contact {
}
private emitLoaded() {
this.emitter.emit('loaded', this.loaded);
if (!this.loaded) {
this.loaded = Boolean(this.revision);
this.emitter.emit('loaded', this.loaded);
}
}
public async setFocus(cardId: string, channelId: string): Promise<Focus> {

View File

@ -143,7 +143,7 @@ export class ContentModule implements Content {
}
private emitLoaded() {
if (this.streamLoaded && this.contentLoaded) {
if (this.streamLoaded && this.contactLoaded) {
this.emitter.emit('loaded', true);
}
}

View File

@ -104,8 +104,6 @@ export class StreamModule {
this.unsealAll = true;
this.syncing = false;
await this.sync();
this.loaded = true;
this.emitLoaded();
}
@ -217,6 +215,7 @@ export class StreamModule {
this.emitChannels();
await this.store.setContentRevision(guid, nextRev);
this.revision = nextRev;
this.emitLoaded();
if (this.nextRevision === nextRev) {
this.nextRevision = null;
}
@ -284,7 +283,10 @@ export class StreamModule {
}
private emitLoaded() {
this.emitter.emit('loaded', this.loaded);
if (!this.loaded) {
this.loaded = Boolean(this.revision);
this.emitter.emit('loaded', this.loaded);
}
}
public async close(): Promise<void> {
@ -301,6 +303,7 @@ export class StreamModule {
public async setRevision(rev: number): Promise<void> {
this.nextRevision = rev;
await this.sync();
this.emitLoaded();
}
public async addSealedChannel(type: string, subject: any, cardIds: string[], aesKeyHex: string, seals: { publicKey: string; sealedKey: string }[]): Promise<string> {