diff --git a/spec/unit/room.spec.ts b/spec/unit/room.spec.ts index 309d43f030d..2a3ef514bca 100644 --- a/spec/unit/room.spec.ts +++ b/spec/unit/room.spec.ts @@ -32,13 +32,14 @@ import { RoomEvent, } from "../../src"; import { EventTimeline } from "../../src/models/event-timeline"; -import { IWrappedReceipt, Room } from "../../src/models/room"; +import { Room } from "../../src/models/room"; import { RoomState } from "../../src/models/room-state"; import { UNSTABLE_ELEMENT_FUNCTIONAL_USERS } from "../../src/@types/event"; import { TestClient } from "../TestClient"; import { emitPromise } from "../test-utils/test-utils"; import { ReceiptType } from "../../src/@types/read_receipts"; import { Thread, ThreadEvent } from "../../src/models/thread"; +import { WrappedReceipt } from "../../src/models/timeline-receipts"; describe("Room", function() { const roomId = "!foo:bar"; @@ -2429,7 +2430,7 @@ describe("Room", function() { it("handles missing receipt type", () => { room.getReadReceiptForUserId = (userId, ignore, receiptType) => { - return receiptType === ReceiptType.ReadPrivate ? { eventId: "eventId" } as IWrappedReceipt : null; + return receiptType === ReceiptType.ReadPrivate ? { eventId: "eventId" } as WrappedReceipt : null; }; expect(room.getEventReadUpTo(userA)).toEqual("eventId"); diff --git a/src/client.ts b/src/client.ts index e1d805d9d88..7631c383c15 100644 --- a/src/client.ts +++ b/src/client.ts @@ -4589,6 +4589,12 @@ export class MatrixClient extends TypedEventEmitter { + if (!event) return; const eventId = event.getId(); const room = this.getRoom(event.getRoomId()); if (room && room.hasPendingEvent(eventId)) { diff --git a/src/http-api.ts b/src/http-api.ts index 3e601c79533..57bd5b15b94 100644 --- a/src/http-api.ts +++ b/src/http-api.ts @@ -390,7 +390,7 @@ export class MatrixHttpApi { }; // set an initial timeout of 30s; we'll advance it each time we get a progress notification - let timeoutTimer = callbacks.setTimeout(timeoutFn, 30000); + let timeoutTimer = callbacks.setTimeout(timeoutFn, 60000); xhr.onreadystatechange = function() { let resp: string; @@ -421,7 +421,7 @@ export class MatrixHttpApi { callbacks.clearTimeout(timeoutTimer); upload.loaded = ev.loaded; upload.total = ev.total; - timeoutTimer = callbacks.setTimeout(timeoutFn, 30000); + timeoutTimer = callbacks.setTimeout(timeoutFn, 60000); if (opts.progressHandler) { opts.progressHandler({ loaded: ev.loaded, diff --git a/src/models/room-member.ts b/src/models/room-member.ts index 2ea13b536ca..4d9ad59f2b3 100644 --- a/src/models/room-member.ts +++ b/src/models/room-member.ts @@ -221,7 +221,7 @@ export class RoomMember extends TypedEventEmitter (both nullable) - }; -}; - // When inserting a visibility event affecting event `eventId`, we // need to scan through existing visibility events for `eventId`. // In theory, this could take an unlimited amount of time if: @@ -207,15 +156,9 @@ export type RoomEventHandlerMap = { [ThreadEvent.New]: (thread: Thread, toStartOfTimeline: boolean) => void; } & ThreadHandlerMap & MatrixEventHandlerMap; -export class Room extends TypedEventEmitter { +export class Room extends TimelineReceipts { public readonly reEmitter: TypedReEmitter; private txnToEvent: Record = {}; // Pending in-flight requests { string: MatrixEvent } - // receipts should clobber based on receipt_type and user_id pairs hence - // the form of this structure. This is sub-optimal for the exposed APIs - // which pass in an event ID and get back some receipts, so we also store - // a pre-cached list for this purpose. - private receipts: Receipts = {}; // { receipt_type: { user_id: IReceipt } } - private receiptCacheByEventId: ReceiptCache = {}; // { event_id: ICachedReceipt[] } private notificationCounts: Partial> = {}; private readonly timelineSets: EventTimelineSet[]; public readonly threadsTimelineSets: EventTimelineSet[] = []; @@ -2592,7 +2535,7 @@ export class Room extends TypedEventEmitter let latest = privateReadReceipt; [unstablePrivateReadReceipt, publicReadReceipt].forEach((receipt) => { - if (receipt?.data?.ts > latest?.data?.ts || !latest) { + if (receipt?.data?.ts > latest?.data?.ts) { latest = receipt; } }); @@ -2658,123 +2601,28 @@ export class Room extends TypedEventEmitter * @param {Boolean} synthetic True if this event is implicit. */ public addReceipt(event: MatrixEvent, synthetic = false): void { - this.addReceiptsToStructure(event, synthetic); - // send events after we've regenerated the structure & cache, otherwise things that - // listened for the event would read stale data. - this.emit(RoomEvent.Receipt, event, this); - } - - /** - * Add a receipt event to the room. - * @param {MatrixEvent} event The m.receipt event. - * @param {Boolean} synthetic True if this event is implicit. - */ - private addReceiptsToStructure(event: MatrixEvent, synthetic: boolean): void { - const content = event.getContent(); - Object.keys(content).forEach((eventId) => { - Object.keys(content[eventId]).forEach((receiptType) => { - Object.keys(content[eventId][receiptType]).forEach((userId) => { - const receipt = content[eventId][receiptType][userId]; - - if (!this.receipts[receiptType]) { - this.receipts[receiptType] = {}; - } - if (!this.receipts[receiptType][userId]) { - this.receipts[receiptType][userId] = [null, null]; - } - - const pair = this.receipts[receiptType][userId]; - - let existingReceipt = pair[ReceiptPairRealIndex]; - if (synthetic) { - existingReceipt = pair[ReceiptPairSyntheticIndex] ?? pair[ReceiptPairRealIndex]; - } - - if (existingReceipt) { - // we only want to add this receipt if we think it is later than the one we already have. - // This is managed server-side, but because we synthesize RRs locally we have to do it here too. - const ordering = this.getUnfilteredTimelineSet().compareEventOrdering( - existingReceipt.eventId, - eventId, - ); - if (ordering !== null && ordering >= 0) { - return; - } - } - - const wrappedReceipt: IWrappedReceipt = { + const content = event.getContent(); + Object.keys(content).forEach((eventId: string) => { + Object.keys(content[eventId]).forEach((receiptType: ReceiptType) => { + Object.keys(content[eventId][receiptType]).forEach((userId: string) => { + // hack, threadId should be thread_id + const receipt = content[eventId][receiptType][userId] as any; + + const receiptDestination = this.threads.get(receipt.thread_id) ?? this; + receiptDestination.addReceiptToStructure( eventId, - data: receipt, - }; - - const realReceipt = synthetic ? pair[ReceiptPairRealIndex] : wrappedReceipt; - const syntheticReceipt = synthetic ? wrappedReceipt : pair[ReceiptPairSyntheticIndex]; - - let ordering: number | null = null; - if (realReceipt && syntheticReceipt) { - ordering = this.getUnfilteredTimelineSet().compareEventOrdering( - realReceipt.eventId, - syntheticReceipt.eventId, - ); - } - - const preferSynthetic = ordering === null || ordering < 0; - - // we don't bother caching just real receipts by event ID as there's nothing that would read it. - // Take the current cached receipt before we overwrite the pair elements. - const cachedReceipt = pair[ReceiptPairSyntheticIndex] ?? pair[ReceiptPairRealIndex]; - - if (synthetic && preferSynthetic) { - pair[ReceiptPairSyntheticIndex] = wrappedReceipt; - } else if (!synthetic) { - pair[ReceiptPairRealIndex] = wrappedReceipt; - - if (!preferSynthetic) { - pair[ReceiptPairSyntheticIndex] = null; - } - } - - const newCachedReceipt = pair[ReceiptPairSyntheticIndex] ?? pair[ReceiptPairRealIndex]; - if (cachedReceipt === newCachedReceipt) return; - - // clean up any previous cache entry - if (cachedReceipt && this.receiptCacheByEventId[cachedReceipt.eventId]) { - const previousEventId = cachedReceipt.eventId; - // Remove the receipt we're about to clobber out of existence from the cache - this.receiptCacheByEventId[previousEventId] = ( - this.receiptCacheByEventId[previousEventId].filter(r => { - return r.type !== receiptType || r.userId !== userId; - }) - ); - - if (this.receiptCacheByEventId[previousEventId].length < 1) { - delete this.receiptCacheByEventId[previousEventId]; // clean up the cache keys - } - } - - // cache the new one - if (!this.receiptCacheByEventId[eventId]) { - this.receiptCacheByEventId[eventId] = []; - } - this.receiptCacheByEventId[eventId].push({ - userId: userId, - type: receiptType as ReceiptType, - data: receipt, - }); + receiptType, + userId, + receipt, + synthetic, + ); }); }); }); - } - /** - * Add a temporary local-echo receipt to the room to reflect in the - * client the fact that we've sent one. - * @param {string} userId The user ID if the receipt sender - * @param {MatrixEvent} e The event that is to be acknowledged - * @param {ReceiptType} receiptType The type of receipt - */ - public addLocalEchoReceipt(userId: string, e: MatrixEvent, receiptType: ReceiptType): void { - this.addReceipt(synthesizeReceipt(userId, e, receiptType), true); + // send events after we've regenerated the structure & cache, otherwise things that + // listened for the event would read stale data. + this.emit(RoomEvent.Receipt, event, this); } /** diff --git a/src/models/thread.ts b/src/models/thread.ts index c451ccb8dc2..d4b01ed2e64 100644 --- a/src/models/thread.ts +++ b/src/models/thread.ts @@ -23,10 +23,10 @@ import { IThreadBundledRelationship, MatrixEvent } from "./event"; import { Direction, EventTimeline } from "./event-timeline"; import { EventTimelineSet, EventTimelineSetHandlerMap } from './event-timeline-set'; import { Room } from './room'; -import { TypedEventEmitter } from "./typed-event-emitter"; import { RoomState } from "./room-state"; import { ServerControlledNamespacedValue } from "../NamespacedValue"; import { logger } from "../logger"; +import { TimelineReceipts } from "./timeline-receipts"; export enum ThreadEvent { New = "Thread.new", @@ -54,7 +54,7 @@ interface IThreadOpts { /** * @experimental */ -export class Thread extends TypedEventEmitter { +export class Thread extends TimelineReceipts { public static hasServerSideSupport: boolean; /** @@ -429,6 +429,18 @@ export class Thread extends TypedEventEmitter { nextBatch, }; } + + public getUnfilteredTimelineSet(): EventTimelineSet { + return this.timelineSet; + } + + public get timeline(): MatrixEvent[] { + return this.events; + } + + public addReceipt(event: MatrixEvent, synthetic: boolean): void { + throw new Error("Unsupported function on the thread model"); + } } export const FILTER_RELATED_BY_SENDERS = new ServerControlledNamespacedValue( diff --git a/src/models/timeline-receipts.ts b/src/models/timeline-receipts.ts new file mode 100644 index 00000000000..90d255067a0 --- /dev/null +++ b/src/models/timeline-receipts.ts @@ -0,0 +1,357 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { ReceiptType } from "../@types/read_receipts"; +import { EventType } from "../matrix"; +import { MatrixEvent, MatrixEventEvent } from "./event"; +import { EventTimelineSet } from "./event-timeline-set"; +import { RoomEvent } from "./room"; +import { ThreadEvent } from "./thread"; +import { ListenerMap, TypedEventEmitter } from "./typed-event-emitter"; +import * as utils from "../utils"; + +export function synthesizeReceipt(userId: string, event: MatrixEvent, receiptType: ReceiptType): MatrixEvent { + // console.log("synthesizing receipt for "+event.getId()); + return new MatrixEvent({ + content: { + [event.getId()]: { + [receiptType]: { + [userId]: { + ts: event.getTs(), + threadId: event.threadRootId ?? undefined, + }, + }, + }, + }, + type: EventType.Receipt, + room_id: event.getRoomId(), + }); +} + +export interface Receipt { + ts: number; + thread_id?: string; +} + +export interface WrappedReceipt { + eventId: string; + data: Receipt; +} + +interface CachedReceipt { + type: ReceiptType; + userId: string; + data: Receipt; +} + +type ReceiptCache = {[eventId: string]: CachedReceipt[]}; + +export interface ReceiptContent { + [eventId: string]: { + [key in ReceiptType]: { + [userId: string]: Receipt; + }; + }; +} + +const ReceiptPairRealIndex = 0; +const ReceiptPairSyntheticIndex = 1; +// We will only hold a synthetic receipt if we do not have a real receipt or the synthetic is newer. +type Receipts = { + [receiptType: string]: { + [userId: string]: [WrappedReceipt, WrappedReceipt]; // Pair (both nullable) + }; +}; + +export type EmittedEvents = RoomEvent + | ThreadEvent.New + | ThreadEvent.Update + | ThreadEvent.NewReply + | RoomEvent.Timeline + | RoomEvent.TimelineReset + | RoomEvent.TimelineRefresh + | RoomEvent.HistoryImportedWithinTimeline + | RoomEvent.OldStateUpdated + | RoomEvent.CurrentStateUpdated + | MatrixEventEvent.BeforeRedaction; + +export abstract class TimelineReceipts< + Events extends string, + Arguments extends ListenerMap, + SuperclassArguments extends ListenerMap = Arguments, +> extends TypedEventEmitter { + // receipts should clobber based on receipt_type and user_id pairs hence + // the form of this structure. This is sub-optimal for the exposed APIs + // which pass in an event ID and get back some receipts, so we also store + // a pre-cached list for this purpose. + private receipts: Receipts = {}; // { receipt_type: { user_id: IReceipt } } + private receiptCacheByEventId: ReceiptCache = {}; // { event_id: ICachedReceipt[] } + + public abstract getUnfilteredTimelineSet(): EventTimelineSet; + public abstract timeline: MatrixEvent[]; + + /** + * Gets the latest receipt for a given user in the room + * @param userId The id of the user for which we want the receipt + * @param ignoreSynthesized Whether to ignore synthesized receipts or not + * @param receiptType Optional. The type of the receipt we want to get + * @returns the latest receipts of the chosen type for the chosen user + */ + public getReadReceiptForUserId( + userId: string, ignoreSynthesized = false, receiptType = ReceiptType.Read, + ): WrappedReceipt | null { + const [realReceipt, syntheticReceipt] = this.receipts[receiptType]?.[userId] ?? []; + if (ignoreSynthesized) { + return realReceipt; + } + + return syntheticReceipt ?? realReceipt; + } + + /** + * Get the ID of the event that a given user has read up to, or null if we + * have received no read receipts from them. + * @param {String} userId The user ID to get read receipt event ID for + * @param {Boolean} ignoreSynthesized If true, return only receipts that have been + * sent by the server, not implicit ones generated + * by the JS SDK. + * @return {String} ID of the latest event that the given user has read, or null. + */ + public getEventReadUpTo(userId: string, ignoreSynthesized = false): string | null { + // XXX: This is very very ugly and I hope I won't have to ever add a new + // receipt type here again. IMHO this should be done by the server in + // some more intelligent manner or the client should just use timestamps + + const timelineSet = this.getUnfilteredTimelineSet(); + const publicReadReceipt = this.getReadReceiptForUserId( + userId, + ignoreSynthesized, + ReceiptType.Read, + ); + const privateReadReceipt = this.getReadReceiptForUserId( + userId, + ignoreSynthesized, + ReceiptType.ReadPrivate, + ); + const unstablePrivateReadReceipt = this.getReadReceiptForUserId( + userId, + ignoreSynthesized, + ReceiptType.UnstableReadPrivate, + ); + + // If we have all, compare them + if (publicReadReceipt?.eventId && privateReadReceipt?.eventId && unstablePrivateReadReceipt?.eventId) { + const comparison1 = timelineSet.compareEventOrdering( + publicReadReceipt.eventId, + privateReadReceipt.eventId, + ); + const comparison2 = timelineSet.compareEventOrdering( + publicReadReceipt.eventId, + unstablePrivateReadReceipt.eventId, + ); + const comparison3 = timelineSet.compareEventOrdering( + privateReadReceipt.eventId, + unstablePrivateReadReceipt.eventId, + ); + if (comparison1 && comparison2 && comparison3) { + return (comparison1 > 0) + ? ((comparison2 > 0) ? publicReadReceipt.eventId : unstablePrivateReadReceipt.eventId) + : ((comparison3 > 0) ? privateReadReceipt.eventId : unstablePrivateReadReceipt.eventId); + } + } + + let latest = privateReadReceipt; + [unstablePrivateReadReceipt, publicReadReceipt].forEach((receipt) => { + if (receipt?.data?.ts > latest?.data?.ts) { + latest = receipt; + } + }); + if (latest?.eventId) return latest?.eventId; + + // The more less likely it is for a read receipt to drift out of date + // the bigger is its precedence + return ( + privateReadReceipt?.eventId ?? + unstablePrivateReadReceipt?.eventId ?? + publicReadReceipt?.eventId ?? + null + ); + } + + public addReceiptToStructure( + eventId: string, + receiptType: ReceiptType, + userId: string, + receipt: Receipt, + synthetic: boolean, + ): void { + if (!this.receipts[receiptType]) { + this.receipts[receiptType] = {}; + } + if (!this.receipts[receiptType][userId]) { + this.receipts[receiptType][userId] = [null, null]; + } + + const pair = this.receipts[receiptType][userId]; + + let existingReceipt = pair[ReceiptPairRealIndex]; + if (synthetic) { + existingReceipt = pair[ReceiptPairSyntheticIndex] ?? pair[ReceiptPairRealIndex]; + } + + if (existingReceipt) { + // we only want to add this receipt if we think it is later than the one we already have. + // This is managed server-side, but because we synthesize RRs locally we have to do it here too. + const ordering = this.getUnfilteredTimelineSet().compareEventOrdering( + existingReceipt.eventId, + eventId, + ); + if (ordering !== null && ordering >= 0) { + return; + } + } + + const wrappedReceipt: WrappedReceipt = { + eventId, + data: receipt, + }; + + const realReceipt = synthetic ? pair[ReceiptPairRealIndex] : wrappedReceipt; + const syntheticReceipt = synthetic ? wrappedReceipt : pair[ReceiptPairSyntheticIndex]; + + let ordering: number | null = null; + if (realReceipt && syntheticReceipt) { + ordering = this.getUnfilteredTimelineSet().compareEventOrdering( + realReceipt.eventId, + syntheticReceipt.eventId, + ); + } + + const preferSynthetic = ordering === null || ordering < 0; + + // we don't bother caching just real receipts by event ID as there's nothing that would read it. + // Take the current cached receipt before we overwrite the pair elements. + const cachedReceipt = pair[ReceiptPairSyntheticIndex] ?? pair[ReceiptPairRealIndex]; + + if (synthetic && preferSynthetic) { + pair[ReceiptPairSyntheticIndex] = wrappedReceipt; + } else if (!synthetic) { + pair[ReceiptPairRealIndex] = wrappedReceipt; + + if (!preferSynthetic) { + pair[ReceiptPairSyntheticIndex] = null; + } + } + + const newCachedReceipt = pair[ReceiptPairSyntheticIndex] ?? pair[ReceiptPairRealIndex]; + if (cachedReceipt === newCachedReceipt) return; + + // clean up any previous cache entry + if (cachedReceipt && this.receiptCacheByEventId[cachedReceipt.eventId]) { + const previousEventId = cachedReceipt.eventId; + // Remove the receipt we're about to clobber out of existence from the cache + this.receiptCacheByEventId[previousEventId] = ( + this.receiptCacheByEventId[previousEventId].filter(r => { + return r.type !== receiptType || r.userId !== userId; + }) + ); + + if (this.receiptCacheByEventId[previousEventId].length < 1) { + delete this.receiptCacheByEventId[previousEventId]; // clean up the cache keys + } + } + + // cache the new one + if (!this.receiptCacheByEventId[eventId]) { + this.receiptCacheByEventId[eventId] = []; + } + this.receiptCacheByEventId[eventId].push({ + userId: userId, + type: receiptType as ReceiptType, + data: receipt, + }); + } + + /** + * Get a list of receipts for the given event. + * @param {MatrixEvent} event the event to get receipts for + * @return {Object[]} A list of receipts with a userId, type and data keys or + * an empty list. + */ + public getReceiptsForEvent(event: MatrixEvent): CachedReceipt[] { + return this.receiptCacheByEventId[event.getId()] || []; + } + + public abstract addReceipt(event: MatrixEvent, synthetic: boolean): void; + + /** + * Add a temporary local-echo receipt to the room to reflect in the + * client the fact that we've sent one. + * @param {string} userId The user ID if the receipt sender + * @param {MatrixEvent} e The event that is to be acknowledged + * @param {ReceiptType} receiptType The type of receipt + */ + public addLocalEchoReceipt(userId: string, e: MatrixEvent, receiptType: ReceiptType): void { + this.addReceipt(synthesizeReceipt(userId, e, receiptType), true); + } + + /** + * Get a list of user IDs who have read up to the given event. + * @param {MatrixEvent} event the event to get read receipts for. + * @return {String[]} A list of user IDs. + */ + public getUsersReadUpTo(event: MatrixEvent): string[] { + return this.getReceiptsForEvent(event).filter(function(receipt) { + return utils.isSupportedReceiptType(receipt.type); + }).map(function(receipt) { + return receipt.userId; + }); + } + + /** + * Determines if the given user has read a particular event ID with the known + * history of the room. This is not a definitive check as it relies only on + * what is available to the room at the time of execution. + * @param {String} userId The user ID to check the read state of. + * @param {String} eventId The event ID to check if the user read. + * @returns {Boolean} True if the user has read the event, false otherwise. + */ + public hasUserReadEvent(userId: string, eventId: string): boolean { + const readUpToId = this.getEventReadUpTo(userId, false); + if (readUpToId === eventId) return true; + + if (this.timeline.length + && this.timeline[this.timeline.length - 1].getSender() + && this.timeline[this.timeline.length - 1].getSender() === userId) { + // It doesn't matter where the event is in the timeline, the user has read + // it because they've sent the latest event. + return true; + } + + for (let i = this.timeline.length - 1; i >= 0; --i) { + const ev = this.timeline[i]; + + // If we encounter the target event first, the user hasn't read it + // however if we encounter the readUpToId first then the user has read + // it. These rules apply because we're iterating bottom-up. + if (ev.getId() === eventId) return false; + if (ev.getId() === readUpToId) return true; + } + + // We don't know if the user has read it, so assume not. + return false; + } +} diff --git a/src/sliding-sync-sdk.ts b/src/sliding-sync-sdk.ts index 74a03b99b45..2d68b198c3d 100644 --- a/src/sliding-sync-sdk.ts +++ b/src/sliding-sync-sdk.ts @@ -250,7 +250,7 @@ export class SlidingSyncSdk { ) { this.opts.initialSyncLimit = this.opts.initialSyncLimit ?? 8; this.opts.resolveInvitesToProfiles = this.opts.resolveInvitesToProfiles || false; - this.opts.pollTimeout = this.opts.pollTimeout || (30 * 1000); + this.opts.pollTimeout = this.opts.pollTimeout || (60 * 1000); this.opts.pendingEventOrdering = this.opts.pendingEventOrdering || PendingEventOrdering.Chronological; this.opts.experimentalThreadSupport = this.opts.experimentalThreadSupport === true; diff --git a/src/sync-accumulator.ts b/src/sync-accumulator.ts index 08686c32d67..7e8c9deccba 100644 --- a/src/sync-accumulator.ts +++ b/src/sync-accumulator.ts @@ -401,7 +401,7 @@ export class SyncAccumulator { // typing forever until someone really does start typing (which // will prompt Synapse to send down an actual m.typing event to // clobber the one we persisted). - if (e.type !== "m.receipt" || !e.content) { + if (e.type !== EventType.Receipt || !e.content) { // This means we'll drop unknown ephemeral events but that // seems okay. return; @@ -546,7 +546,7 @@ export class SyncAccumulator { // Add receipt data const receiptEvent = { - type: "m.receipt", + type: EventType.Receipt, room_id: roomId, content: { // $event_id: { "m.read": { $user_id: $json } } diff --git a/src/sync.ts b/src/sync.ts index 4abd4fb5bb2..a16f08f8925 100644 --- a/src/sync.ts +++ b/src/sync.ts @@ -176,7 +176,7 @@ export class SyncApi { constructor(private readonly client: MatrixClient, private readonly opts: Partial = {}) { this.opts.initialSyncLimit = this.opts.initialSyncLimit ?? 8; this.opts.resolveInvitesToProfiles = this.opts.resolveInvitesToProfiles || false; - this.opts.pollTimeout = this.opts.pollTimeout || (30 * 1000); + this.opts.pollTimeout = this.opts.pollTimeout || (60 * 1000); this.opts.pendingEventOrdering = this.opts.pendingEventOrdering || PendingEventOrdering.Chronological; this.opts.experimentalThreadSupport = this.opts.experimentalThreadSupport === true; @@ -505,7 +505,7 @@ export class SyncApi { // FIXME: gut wrenching; hard-coded timeout values this.client.http.authedRequest(undefined, Method.Get, "/events", { room_id: peekRoom.roomId, - timeout: String(30 * 1000), + timeout: String(60 * 1000), from: token, }, undefined, 50 * 1000).then((res) => { if (this._peekRoom !== peekRoom) {