forked from Mirrors/gomuks
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
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
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
|
||||
)
|
||||
|
||||
|
|
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=
|
||||
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/mautrix v0.22.1-0.20241118181122-363fdfa3b227 h1:VsOldmYtN1NYh4yh2P5U+PemDsBB/zqD8z/Zqml76s0=
|
||||
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 h1:8zrcggLi5IyHO9oBxjejbRyvgT0ePEwYLdFCdtohDnI=
|
||||
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/go.mod h1:yQgaGQ1rFtJUzkmKiHYSSfuQxqfYmd//X6PxvholpeE=
|
||||
|
|
|
@ -33,7 +33,7 @@ import (
|
|||
"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)
|
||||
if err != nil {
|
||||
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")
|
||||
}
|
||||
}
|
||||
initData, initErr := json.Marshal(gmx.Client.State())
|
||||
if initErr != nil {
|
||||
log.Err(initErr).Msg("Failed to marshal init message")
|
||||
return
|
||||
}
|
||||
initErr = writeCmd(ctx, conn, &hicli.JSONCommand{
|
||||
initErr := writeCmd(ctx, conn, &hicli.JSONCommandCustom[*hicli.ClientState]{
|
||||
Command: "client_state",
|
||||
Data: initData,
|
||||
Data: gmx.Client.State(),
|
||||
})
|
||||
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
|
||||
}
|
||||
go sendImageAuthToken()
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
package hicli
|
||||
|
||||
import (
|
||||
"go.mau.fi/util/jsontime"
|
||||
"maunium.net/go/mautrix/event"
|
||||
"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
|
||||
}
|
||||
|
||||
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 {
|
||||
RoomID id.RoomID `json:"room_id"`
|
||||
PreviewEventRowID database.EventRowID `json:"preview_event_rowid,omitempty"`
|
||||
|
|
|
@ -44,7 +44,10 @@ type HiClient struct {
|
|||
KeyBackupVersion id.KeyBackupVersion
|
||||
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)
|
||||
|
||||
|
@ -87,6 +90,7 @@ func New(rawDB, cryptoDB *dbutil.Database, log zerolog.Logger, pickleKey []byte,
|
|||
|
||||
EventHandler: evtHandler,
|
||||
}
|
||||
c.SyncStatus.Store(syncWaiting)
|
||||
c.ClientStore = &database.ClientStateStore{Database: db}
|
||||
c.Client = &mautrix.Client{
|
||||
UserAgent: mautrix.DefaultUserAgent,
|
||||
|
@ -243,8 +247,10 @@ func (h *HiClient) Sync() {
|
|||
log.Info().Msg("Starting syncing")
|
||||
err := h.Client.SyncWithContext(ctx)
|
||||
if err != nil && ctx.Err() == nil {
|
||||
h.markSyncErrored(err)
|
||||
log.Err(err).Msg("Fatal error in syncer")
|
||||
} else {
|
||||
h.SyncStatus.Store(syncWaiting)
|
||||
log.Info().Msg("Syncing stopped")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,12 +16,14 @@ import (
|
|||
"go.mau.fi/util/exerrors"
|
||||
)
|
||||
|
||||
type JSONCommand struct {
|
||||
Command string `json:"command"`
|
||||
RequestID int64 `json:"request_id"`
|
||||
Data json.RawMessage `json:"data"`
|
||||
type JSONCommandCustom[T any] struct {
|
||||
Command string `json:"command"`
|
||||
RequestID int64 `json:"request_id"`
|
||||
Data T `json:"data"`
|
||||
}
|
||||
|
||||
type JSONCommand = JSONCommandCustom[json.RawMessage]
|
||||
|
||||
type JSONEventHandler func(*JSONCommand)
|
||||
|
||||
var outgoingEventCounter atomic.Int64
|
||||
|
@ -31,6 +33,8 @@ func (jeh JSONEventHandler) HandleEvent(evt any) {
|
|||
switch evt.(type) {
|
||||
case *SyncComplete:
|
||||
command = "sync_complete"
|
||||
case *SyncStatus:
|
||||
command = "sync_status"
|
||||
case *EventsDecrypted:
|
||||
command = "events_decrypted"
|
||||
case *Typing:
|
||||
|
|
|
@ -39,6 +39,28 @@ type syncContext struct {
|
|||
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 {
|
||||
log := zerolog.Ctx(ctx)
|
||||
postponedToDevices := resp.ToDevice.Events[:0]
|
||||
|
|
|
@ -27,6 +27,7 @@ const (
|
|||
|
||||
func (h *hiSyncer) ProcessResponse(ctx context.Context, resp *mautrix.RespSync, since string) error {
|
||||
c := (*HiClient)(h)
|
||||
c.lastSync = time.Now()
|
||||
ctx = context.WithValue(ctx, syncContextKey, &syncContext{evt: &SyncComplete{
|
||||
Rooms: make(map[id.RoomID]*SyncRoom, len(resp.Rooms.Join)),
|
||||
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
|
||||
}
|
||||
c.postProcessSyncResponse(ctx, resp, since)
|
||||
c.syncErrors = 0
|
||||
c.markSyncOK()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *hiSyncer) OnFailedSync(_ *mautrix.RespSync, err error) (time.Duration, error) {
|
||||
(*HiClient)(h).Log.Err(err).Msg("Sync failed, retrying in 1 second")
|
||||
return 1 * time.Second, nil
|
||||
c := (*HiClient)(h)
|
||||
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 {
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
//
|
||||
// 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/>.
|
||||
import { CachedEventDispatcher } from "../util/eventdispatcher.ts"
|
||||
import { CachedEventDispatcher, NonNullCachedEventDispatcher } from "../util/eventdispatcher.ts"
|
||||
import RPCClient, { SendMessageParams } from "./rpc.ts"
|
||||
import { RoomStateStore, StateStore } from "./statestore"
|
||||
import type {
|
||||
|
@ -25,11 +25,13 @@ import type {
|
|||
RPCEvent,
|
||||
RoomID,
|
||||
RoomStateGUID,
|
||||
SyncStatus,
|
||||
UserID,
|
||||
} from "./types"
|
||||
|
||||
export default class Client {
|
||||
readonly state = new CachedEventDispatcher<ClientState>()
|
||||
readonly syncStatus = new NonNullCachedEventDispatcher<SyncStatus>({ type: "waiting", error_count: 0 })
|
||||
readonly store = new StateStore()
|
||||
#stateRequests: RoomStateGUID[] = []
|
||||
#stateRequestQueued = false
|
||||
|
@ -82,6 +84,8 @@ export default class Client {
|
|||
#handleEvent = (ev: RPCEvent) => {
|
||||
if (ev.command === "client_state") {
|
||||
this.state.emit(ev.data)
|
||||
} else if (ev.command === "sync_status") {
|
||||
this.syncStatus.emit(ev.data)
|
||||
} else if (ev.command === "sync_complete") {
|
||||
this.store.applySync(ev.data)
|
||||
} else if (ev.command === "events_decrypted") {
|
||||
|
|
|
@ -107,8 +107,20 @@ export interface ClientStateEvent extends RPCCommand<ClientState> {
|
|||
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 =
|
||||
ClientStateEvent |
|
||||
SyncStatusEvent |
|
||||
TypingEvent |
|
||||
SendCompleteEvent |
|
||||
EventsDecryptedEvent |
|
||||
|
|
|
@ -47,3 +47,22 @@ main.matrix-main {
|
|||
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
|
||||
// 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 { RoomStateStore } from "@/api/statestore"
|
||||
import type { RoomID } from "@/api/types"
|
||||
import { useEventAsState } from "@/util/eventdispatcher.ts"
|
||||
import ClientContext from "./ClientContext.ts"
|
||||
import MainScreenContext, { MainScreenContextFields } from "./MainScreenContext.ts"
|
||||
import StylePreferences from "./StylePreferences.tsx"
|
||||
|
@ -102,10 +104,13 @@ class ContextFields implements MainScreenContextFields {
|
|||
closeRightPanel = () => this.setRightPanel(null)
|
||||
}
|
||||
|
||||
const SYNC_ERROR_HIDE_DELAY = 30 * 1000
|
||||
|
||||
const MainScreen = () => {
|
||||
const [activeRoom, directSetActiveRoom] = useState<RoomStateStore | null>(null)
|
||||
const [rightPanel, setRightPanel] = useReducer(rpReducer, null)
|
||||
const client = use(ClientContext)!
|
||||
const syncStatus = useEventAsState(client.syncStatus)
|
||||
const context = useMemo(
|
||||
() => new ContextFields(setRightPanel, directSetActiveRoom, client),
|
||||
[client],
|
||||
|
@ -144,9 +149,24 @@ const MainScreen = () => {
|
|||
if (rightPanel) {
|
||||
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}>
|
||||
<ModalWrapper>
|
||||
<StylePreferences client={client} activeRoom={activeRoom} />
|
||||
<StylePreferences client={client} activeRoom={activeRoom}/>
|
||||
<main className={classNames.join(" ")} style={extraStyle}>
|
||||
<RoomList activeRoomID={activeRoom?.roomID ?? null}/>
|
||||
{resizeHandle1}
|
||||
|
@ -162,6 +182,7 @@ const MainScreen = () => {
|
|||
{rightPanel && <RightPanel {...rightPanel}/>}
|
||||
</>}
|
||||
</main>
|
||||
{syncLoader}
|
||||
</ModalWrapper>
|
||||
</MainScreenContext>
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue