mirror of
https://github.com/tulir/gomuks.git
synced 2025-04-19 18:13:41 -05:00
parent
74e97c5c8c
commit
f3717505bf
12 changed files with 139 additions and 22 deletions
2
go.mod
2
go.mod
|
@ -24,7 +24,7 @@ require (
|
||||||
golang.org/x/text v0.20.0
|
golang.org/x/text v0.20.0
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
maunium.net/go/mauflag v1.0.0
|
maunium.net/go/mauflag v1.0.0
|
||||||
maunium.net/go/mautrix v0.22.1-0.20241118181122-363fdfa3b227
|
maunium.net/go/mautrix v0.22.1-0.20241120171618-4cd2bb62ff01
|
||||||
mvdan.cc/xurls/v2 v2.5.0
|
mvdan.cc/xurls/v2 v2.5.0
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
4
go.sum
4
go.sum
|
@ -89,7 +89,7 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
maunium.net/go/mauflag v1.0.0 h1:YiaRc0tEI3toYtJMRIfjP+jklH45uDHtT80nUamyD4M=
|
maunium.net/go/mauflag v1.0.0 h1:YiaRc0tEI3toYtJMRIfjP+jklH45uDHtT80nUamyD4M=
|
||||||
maunium.net/go/mauflag v1.0.0/go.mod h1:nLivPOpTpHnpzEh8jEdSL9UqO9+/KBJFmNRlwKfkPeA=
|
maunium.net/go/mauflag v1.0.0/go.mod h1:nLivPOpTpHnpzEh8jEdSL9UqO9+/KBJFmNRlwKfkPeA=
|
||||||
maunium.net/go/mautrix v0.22.1-0.20241118181122-363fdfa3b227 h1:VsOldmYtN1NYh4yh2P5U+PemDsBB/zqD8z/Zqml76s0=
|
maunium.net/go/mautrix v0.22.1-0.20241120171618-4cd2bb62ff01 h1:8zrcggLi5IyHO9oBxjejbRyvgT0ePEwYLdFCdtohDnI=
|
||||||
maunium.net/go/mautrix v0.22.1-0.20241118181122-363fdfa3b227/go.mod h1:oqwf9WYC/brqucM+heYk4gX11O59nP+ljvyxVhndFIM=
|
maunium.net/go/mautrix v0.22.1-0.20241120171618-4cd2bb62ff01/go.mod h1:oqwf9WYC/brqucM+heYk4gX11O59nP+ljvyxVhndFIM=
|
||||||
mvdan.cc/xurls/v2 v2.5.0 h1:lyBNOm8Wo71UknhUs4QTFUNNMyxy2JEIaKKo0RWOh+8=
|
mvdan.cc/xurls/v2 v2.5.0 h1:lyBNOm8Wo71UknhUs4QTFUNNMyxy2JEIaKKo0RWOh+8=
|
||||||
mvdan.cc/xurls/v2 v2.5.0/go.mod h1:yQgaGQ1rFtJUzkmKiHYSSfuQxqfYmd//X6PxvholpeE=
|
mvdan.cc/xurls/v2 v2.5.0/go.mod h1:yQgaGQ1rFtJUzkmKiHYSSfuQxqfYmd//X6PxvholpeE=
|
||||||
|
|
|
@ -33,7 +33,7 @@ import (
|
||||||
"go.mau.fi/gomuks/pkg/hicli"
|
"go.mau.fi/gomuks/pkg/hicli"
|
||||||
)
|
)
|
||||||
|
|
||||||
func writeCmd(ctx context.Context, conn *websocket.Conn, cmd *hicli.JSONCommand) error {
|
func writeCmd[T any](ctx context.Context, conn *websocket.Conn, cmd *hicli.JSONCommandCustom[T]) error {
|
||||||
writer, err := conn.Writer(ctx, websocket.MessageText)
|
writer, err := conn.Writer(ctx, websocket.MessageText)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -188,17 +188,20 @@ func (gmx *Gomuks) HandleWebsocket(w http.ResponseWriter, r *http.Request) {
|
||||||
log.Trace().Int64("req_id", cmd.RequestID).Msg("Sent response to command")
|
log.Trace().Int64("req_id", cmd.RequestID).Msg("Sent response to command")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
initData, initErr := json.Marshal(gmx.Client.State())
|
initErr := writeCmd(ctx, conn, &hicli.JSONCommandCustom[*hicli.ClientState]{
|
||||||
if initErr != nil {
|
|
||||||
log.Err(initErr).Msg("Failed to marshal init message")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
initErr = writeCmd(ctx, conn, &hicli.JSONCommand{
|
|
||||||
Command: "client_state",
|
Command: "client_state",
|
||||||
Data: initData,
|
Data: gmx.Client.State(),
|
||||||
})
|
})
|
||||||
if initErr != nil {
|
if initErr != nil {
|
||||||
log.Err(initErr).Msg("Failed to write init message")
|
log.Err(initErr).Msg("Failed to write init client state message")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
initErr = writeCmd(ctx, conn, &hicli.JSONCommandCustom[*hicli.SyncStatus]{
|
||||||
|
Command: "sync_status",
|
||||||
|
Data: gmx.Client.SyncStatus.Load(),
|
||||||
|
})
|
||||||
|
if initErr != nil {
|
||||||
|
log.Err(initErr).Msg("Failed to write init sync status message")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
go sendImageAuthToken()
|
go sendImageAuthToken()
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
package hicli
|
package hicli
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"go.mau.fi/util/jsontime"
|
||||||
"maunium.net/go/mautrix/event"
|
"maunium.net/go/mautrix/event"
|
||||||
"maunium.net/go/mautrix/id"
|
"maunium.net/go/mautrix/id"
|
||||||
|
|
||||||
|
@ -38,6 +39,21 @@ func (c *SyncComplete) IsEmpty() bool {
|
||||||
return len(c.Rooms) == 0 && len(c.LeftRooms) == 0 && len(c.AccountData) == 0
|
return len(c.Rooms) == 0 && len(c.LeftRooms) == 0 && len(c.AccountData) == 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SyncStatusType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
SyncStatusOK SyncStatusType = "ok"
|
||||||
|
SyncStatusWaiting SyncStatusType = "waiting"
|
||||||
|
SyncStatusErrored SyncStatusType = "errored"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SyncStatus struct {
|
||||||
|
Type SyncStatusType `json:"type"`
|
||||||
|
Error string `json:"error,omitempty"`
|
||||||
|
ErrorCount int `json:"error_count"`
|
||||||
|
LastSync jsontime.UnixMilli `json:"last_sync,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
type EventsDecrypted struct {
|
type EventsDecrypted struct {
|
||||||
RoomID id.RoomID `json:"room_id"`
|
RoomID id.RoomID `json:"room_id"`
|
||||||
PreviewEventRowID database.EventRowID `json:"preview_event_rowid,omitempty"`
|
PreviewEventRowID database.EventRowID `json:"preview_event_rowid,omitempty"`
|
||||||
|
|
|
@ -44,7 +44,10 @@ type HiClient struct {
|
||||||
KeyBackupVersion id.KeyBackupVersion
|
KeyBackupVersion id.KeyBackupVersion
|
||||||
KeyBackupKey *backup.MegolmBackupKey
|
KeyBackupKey *backup.MegolmBackupKey
|
||||||
|
|
||||||
PushRules atomic.Pointer[pushrules.PushRuleset]
|
PushRules atomic.Pointer[pushrules.PushRuleset]
|
||||||
|
SyncStatus atomic.Pointer[SyncStatus]
|
||||||
|
syncErrors int
|
||||||
|
lastSync time.Time
|
||||||
|
|
||||||
EventHandler func(evt any)
|
EventHandler func(evt any)
|
||||||
|
|
||||||
|
@ -87,6 +90,7 @@ func New(rawDB, cryptoDB *dbutil.Database, log zerolog.Logger, pickleKey []byte,
|
||||||
|
|
||||||
EventHandler: evtHandler,
|
EventHandler: evtHandler,
|
||||||
}
|
}
|
||||||
|
c.SyncStatus.Store(syncWaiting)
|
||||||
c.ClientStore = &database.ClientStateStore{Database: db}
|
c.ClientStore = &database.ClientStateStore{Database: db}
|
||||||
c.Client = &mautrix.Client{
|
c.Client = &mautrix.Client{
|
||||||
UserAgent: mautrix.DefaultUserAgent,
|
UserAgent: mautrix.DefaultUserAgent,
|
||||||
|
@ -243,8 +247,10 @@ func (h *HiClient) Sync() {
|
||||||
log.Info().Msg("Starting syncing")
|
log.Info().Msg("Starting syncing")
|
||||||
err := h.Client.SyncWithContext(ctx)
|
err := h.Client.SyncWithContext(ctx)
|
||||||
if err != nil && ctx.Err() == nil {
|
if err != nil && ctx.Err() == nil {
|
||||||
|
h.markSyncErrored(err)
|
||||||
log.Err(err).Msg("Fatal error in syncer")
|
log.Err(err).Msg("Fatal error in syncer")
|
||||||
} else {
|
} else {
|
||||||
|
h.SyncStatus.Store(syncWaiting)
|
||||||
log.Info().Msg("Syncing stopped")
|
log.Info().Msg("Syncing stopped")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,12 +16,14 @@ import (
|
||||||
"go.mau.fi/util/exerrors"
|
"go.mau.fi/util/exerrors"
|
||||||
)
|
)
|
||||||
|
|
||||||
type JSONCommand struct {
|
type JSONCommandCustom[T any] struct {
|
||||||
Command string `json:"command"`
|
Command string `json:"command"`
|
||||||
RequestID int64 `json:"request_id"`
|
RequestID int64 `json:"request_id"`
|
||||||
Data json.RawMessage `json:"data"`
|
Data T `json:"data"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type JSONCommand = JSONCommandCustom[json.RawMessage]
|
||||||
|
|
||||||
type JSONEventHandler func(*JSONCommand)
|
type JSONEventHandler func(*JSONCommand)
|
||||||
|
|
||||||
var outgoingEventCounter atomic.Int64
|
var outgoingEventCounter atomic.Int64
|
||||||
|
@ -31,6 +33,8 @@ func (jeh JSONEventHandler) HandleEvent(evt any) {
|
||||||
switch evt.(type) {
|
switch evt.(type) {
|
||||||
case *SyncComplete:
|
case *SyncComplete:
|
||||||
command = "sync_complete"
|
command = "sync_complete"
|
||||||
|
case *SyncStatus:
|
||||||
|
command = "sync_status"
|
||||||
case *EventsDecrypted:
|
case *EventsDecrypted:
|
||||||
command = "events_decrypted"
|
command = "events_decrypted"
|
||||||
case *Typing:
|
case *Typing:
|
||||||
|
|
|
@ -39,6 +39,28 @@ type syncContext struct {
|
||||||
evt *SyncComplete
|
evt *SyncComplete
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *HiClient) markSyncErrored(err error) {
|
||||||
|
stat := &SyncStatus{
|
||||||
|
Type: SyncStatusErrored,
|
||||||
|
Error: err.Error(),
|
||||||
|
ErrorCount: h.syncErrors,
|
||||||
|
LastSync: jsontime.UM(h.lastSync),
|
||||||
|
}
|
||||||
|
h.SyncStatus.Store(stat)
|
||||||
|
h.EventHandler(stat)
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
syncOK = &SyncStatus{Type: SyncStatusOK}
|
||||||
|
syncWaiting = &SyncStatus{Type: SyncStatusWaiting}
|
||||||
|
)
|
||||||
|
|
||||||
|
func (h *HiClient) markSyncOK() {
|
||||||
|
if h.SyncStatus.Swap(syncOK) != syncOK {
|
||||||
|
h.EventHandler(syncOK)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (h *HiClient) preProcessSyncResponse(ctx context.Context, resp *mautrix.RespSync, since string) error {
|
func (h *HiClient) preProcessSyncResponse(ctx context.Context, resp *mautrix.RespSync, since string) error {
|
||||||
log := zerolog.Ctx(ctx)
|
log := zerolog.Ctx(ctx)
|
||||||
postponedToDevices := resp.ToDevice.Events[:0]
|
postponedToDevices := resp.ToDevice.Events[:0]
|
||||||
|
|
|
@ -27,6 +27,7 @@ const (
|
||||||
|
|
||||||
func (h *hiSyncer) ProcessResponse(ctx context.Context, resp *mautrix.RespSync, since string) error {
|
func (h *hiSyncer) ProcessResponse(ctx context.Context, resp *mautrix.RespSync, since string) error {
|
||||||
c := (*HiClient)(h)
|
c := (*HiClient)(h)
|
||||||
|
c.lastSync = time.Now()
|
||||||
ctx = context.WithValue(ctx, syncContextKey, &syncContext{evt: &SyncComplete{
|
ctx = context.WithValue(ctx, syncContextKey, &syncContext{evt: &SyncComplete{
|
||||||
Rooms: make(map[id.RoomID]*SyncRoom, len(resp.Rooms.Join)),
|
Rooms: make(map[id.RoomID]*SyncRoom, len(resp.Rooms.Join)),
|
||||||
LeftRooms: make([]id.RoomID, 0, len(resp.Rooms.Leave)),
|
LeftRooms: make([]id.RoomID, 0, len(resp.Rooms.Leave)),
|
||||||
|
@ -42,12 +43,21 @@ func (h *hiSyncer) ProcessResponse(ctx context.Context, resp *mautrix.RespSync,
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
c.postProcessSyncResponse(ctx, resp, since)
|
c.postProcessSyncResponse(ctx, resp, since)
|
||||||
|
c.syncErrors = 0
|
||||||
|
c.markSyncOK()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *hiSyncer) OnFailedSync(_ *mautrix.RespSync, err error) (time.Duration, error) {
|
func (h *hiSyncer) OnFailedSync(_ *mautrix.RespSync, err error) (time.Duration, error) {
|
||||||
(*HiClient)(h).Log.Err(err).Msg("Sync failed, retrying in 1 second")
|
c := (*HiClient)(h)
|
||||||
return 1 * time.Second, nil
|
c.syncErrors++
|
||||||
|
delay := 1 * time.Second
|
||||||
|
if c.syncErrors > 5 {
|
||||||
|
delay = max(time.Duration(c.syncErrors)*time.Second, 30*time.Second)
|
||||||
|
}
|
||||||
|
c.markSyncErrored(err)
|
||||||
|
c.Log.Err(err).Dur("retry_in", delay).Msg("Sync failed")
|
||||||
|
return delay, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *hiSyncer) GetFilterJSON(_ id.UserID) *mautrix.Filter {
|
func (h *hiSyncer) GetFilterJSON(_ id.UserID) *mautrix.Filter {
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
//
|
//
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
import { CachedEventDispatcher } from "../util/eventdispatcher.ts"
|
import { CachedEventDispatcher, NonNullCachedEventDispatcher } from "../util/eventdispatcher.ts"
|
||||||
import RPCClient, { SendMessageParams } from "./rpc.ts"
|
import RPCClient, { SendMessageParams } from "./rpc.ts"
|
||||||
import { RoomStateStore, StateStore } from "./statestore"
|
import { RoomStateStore, StateStore } from "./statestore"
|
||||||
import type {
|
import type {
|
||||||
|
@ -25,11 +25,13 @@ import type {
|
||||||
RPCEvent,
|
RPCEvent,
|
||||||
RoomID,
|
RoomID,
|
||||||
RoomStateGUID,
|
RoomStateGUID,
|
||||||
|
SyncStatus,
|
||||||
UserID,
|
UserID,
|
||||||
} from "./types"
|
} from "./types"
|
||||||
|
|
||||||
export default class Client {
|
export default class Client {
|
||||||
readonly state = new CachedEventDispatcher<ClientState>()
|
readonly state = new CachedEventDispatcher<ClientState>()
|
||||||
|
readonly syncStatus = new NonNullCachedEventDispatcher<SyncStatus>({ type: "waiting", error_count: 0 })
|
||||||
readonly store = new StateStore()
|
readonly store = new StateStore()
|
||||||
#stateRequests: RoomStateGUID[] = []
|
#stateRequests: RoomStateGUID[] = []
|
||||||
#stateRequestQueued = false
|
#stateRequestQueued = false
|
||||||
|
@ -82,6 +84,8 @@ export default class Client {
|
||||||
#handleEvent = (ev: RPCEvent) => {
|
#handleEvent = (ev: RPCEvent) => {
|
||||||
if (ev.command === "client_state") {
|
if (ev.command === "client_state") {
|
||||||
this.state.emit(ev.data)
|
this.state.emit(ev.data)
|
||||||
|
} else if (ev.command === "sync_status") {
|
||||||
|
this.syncStatus.emit(ev.data)
|
||||||
} else if (ev.command === "sync_complete") {
|
} else if (ev.command === "sync_complete") {
|
||||||
this.store.applySync(ev.data)
|
this.store.applySync(ev.data)
|
||||||
} else if (ev.command === "events_decrypted") {
|
} else if (ev.command === "events_decrypted") {
|
||||||
|
|
|
@ -107,8 +107,20 @@ export interface ClientStateEvent extends RPCCommand<ClientState> {
|
||||||
command: "client_state"
|
command: "client_state"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SyncStatus {
|
||||||
|
type: "ok" | "waiting" | "errored"
|
||||||
|
error?: string
|
||||||
|
error_count: number
|
||||||
|
last_sync?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SyncStatusEvent extends RPCCommand<SyncStatus> {
|
||||||
|
command: "sync_status"
|
||||||
|
}
|
||||||
|
|
||||||
export type RPCEvent =
|
export type RPCEvent =
|
||||||
ClientStateEvent |
|
ClientStateEvent |
|
||||||
|
SyncStatusEvent |
|
||||||
TypingEvent |
|
TypingEvent |
|
||||||
SendCompleteEvent |
|
SendCompleteEvent |
|
||||||
EventsDecryptedEvent |
|
EventsDecryptedEvent |
|
||||||
|
|
|
@ -47,3 +47,22 @@ main.matrix-main {
|
||||||
grid-area: rh2;
|
grid-area: rh2;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
div.sync-status {
|
||||||
|
position: fixed;
|
||||||
|
top: 1rem;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, 0);
|
||||||
|
padding: 1.5rem;
|
||||||
|
background-color: var(--background-color);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: .5rem;
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
|
||||||
|
&.errored {
|
||||||
|
border: 2px solid var(--error-color);
|
||||||
|
color: var(--error-color);
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -13,10 +13,12 @@
|
||||||
//
|
//
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
import { use, useEffect, useInsertionEffect, useLayoutEffect, useMemo, useReducer, useState } from "react"
|
import { JSX, use, useEffect, useInsertionEffect, useLayoutEffect, useMemo, useReducer, useState } from "react"
|
||||||
|
import { SyncLoader } from "react-spinners"
|
||||||
import Client from "@/api/client.ts"
|
import Client from "@/api/client.ts"
|
||||||
import { RoomStateStore } from "@/api/statestore"
|
import { RoomStateStore } from "@/api/statestore"
|
||||||
import type { RoomID } from "@/api/types"
|
import type { RoomID } from "@/api/types"
|
||||||
|
import { useEventAsState } from "@/util/eventdispatcher.ts"
|
||||||
import ClientContext from "./ClientContext.ts"
|
import ClientContext from "./ClientContext.ts"
|
||||||
import MainScreenContext, { MainScreenContextFields } from "./MainScreenContext.ts"
|
import MainScreenContext, { MainScreenContextFields } from "./MainScreenContext.ts"
|
||||||
import StylePreferences from "./StylePreferences.tsx"
|
import StylePreferences from "./StylePreferences.tsx"
|
||||||
|
@ -102,10 +104,13 @@ class ContextFields implements MainScreenContextFields {
|
||||||
closeRightPanel = () => this.setRightPanel(null)
|
closeRightPanel = () => this.setRightPanel(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const SYNC_ERROR_HIDE_DELAY = 30 * 1000
|
||||||
|
|
||||||
const MainScreen = () => {
|
const MainScreen = () => {
|
||||||
const [activeRoom, directSetActiveRoom] = useState<RoomStateStore | null>(null)
|
const [activeRoom, directSetActiveRoom] = useState<RoomStateStore | null>(null)
|
||||||
const [rightPanel, setRightPanel] = useReducer(rpReducer, null)
|
const [rightPanel, setRightPanel] = useReducer(rpReducer, null)
|
||||||
const client = use(ClientContext)!
|
const client = use(ClientContext)!
|
||||||
|
const syncStatus = useEventAsState(client.syncStatus)
|
||||||
const context = useMemo(
|
const context = useMemo(
|
||||||
() => new ContextFields(setRightPanel, directSetActiveRoom, client),
|
() => new ContextFields(setRightPanel, directSetActiveRoom, client),
|
||||||
[client],
|
[client],
|
||||||
|
@ -144,9 +149,24 @@ const MainScreen = () => {
|
||||||
if (rightPanel) {
|
if (rightPanel) {
|
||||||
classNames.push("right-panel-open")
|
classNames.push("right-panel-open")
|
||||||
}
|
}
|
||||||
|
let syncLoader: JSX.Element | null = null
|
||||||
|
if (syncStatus.type === "waiting") {
|
||||||
|
syncLoader = <div className="sync-status waiting">
|
||||||
|
<SyncLoader color="var(--primary-color)"/>
|
||||||
|
Waiting for first sync...
|
||||||
|
</div>
|
||||||
|
} else if (
|
||||||
|
syncStatus.type === "errored"
|
||||||
|
&& (syncStatus.error_count > 2 || (syncStatus.last_sync ?? 0) + SYNC_ERROR_HIDE_DELAY < Date.now())
|
||||||
|
) {
|
||||||
|
syncLoader = <div className="sync-status errored" title={syncStatus.error}>
|
||||||
|
<SyncLoader color="var(--error-color)"/>
|
||||||
|
Sync is failing
|
||||||
|
</div>
|
||||||
|
}
|
||||||
return <MainScreenContext value={context}>
|
return <MainScreenContext value={context}>
|
||||||
<ModalWrapper>
|
<ModalWrapper>
|
||||||
<StylePreferences client={client} activeRoom={activeRoom} />
|
<StylePreferences client={client} activeRoom={activeRoom}/>
|
||||||
<main className={classNames.join(" ")} style={extraStyle}>
|
<main className={classNames.join(" ")} style={extraStyle}>
|
||||||
<RoomList activeRoomID={activeRoom?.roomID ?? null}/>
|
<RoomList activeRoomID={activeRoom?.roomID ?? null}/>
|
||||||
{resizeHandle1}
|
{resizeHandle1}
|
||||||
|
@ -162,6 +182,7 @@ const MainScreen = () => {
|
||||||
{rightPanel && <RightPanel {...rightPanel}/>}
|
{rightPanel && <RightPanel {...rightPanel}/>}
|
||||||
</>}
|
</>}
|
||||||
</main>
|
</main>
|
||||||
|
{syncLoader}
|
||||||
</ModalWrapper>
|
</ModalWrapper>
|
||||||
</MainScreenContext>
|
</MainScreenContext>
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue