mirror of
				https://github.com/vector-im/element-web.git
				synced 2025-11-04 02:02:14 +01:00 
			
		
		
		
	Merge branch 'develop' into gsouquet-scroll-to-live-reset-hash
This commit is contained in:
		
						commit
						7627ea13fe
					
				@ -124,12 +124,19 @@ all kinds of filtering.
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
## Filtering
 | 
					## Filtering
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Filters are provided to the store as condition classes, which are then passed along to the algorithm
 | 
					Filters are provided to the store as condition classes and have two major kinds: Prefilters and Runtime.
 | 
				
			||||||
implementations. The implementations then get to decide how to actually filter the rooms, however in
 | 
					 | 
				
			||||||
practice the base `Algorithm` class deals with the filtering in a more optimized/generic way.
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
The results of filters get cached to avoid needlessly iterating over potentially thousands of rooms,
 | 
					Prefilters flush out rooms which shouldn't appear to the algorithm implementations. Typically this is
 | 
				
			||||||
as the old room list store does. When a filter condition changes, it emits an update which (in this
 | 
					due to some higher order room list filtering (such as spaces or tags) deliberately exposing a subset of
 | 
				
			||||||
 | 
					rooms to the user. The algorithm implementations will not see a room being prefiltered out.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Runtime filters are used for more dynamic filtering, such as the user filtering by room name. These
 | 
				
			||||||
 | 
					filters are passed along to the algorithm implementations where those implementations decide how and
 | 
				
			||||||
 | 
					when to apply the filter. In practice, the base `Algorithm` class ends up doing the heavy lifting for
 | 
				
			||||||
 | 
					optimization reasons.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					The results of runtime filters get cached to avoid needlessly iterating over potentially thousands of
 | 
				
			||||||
 | 
					rooms, as the old room list store does. When a filter condition changes, it emits an update which (in this
 | 
				
			||||||
case) the `Algorithm` class will pick up and act accordingly. Typically, this also means filtering a
 | 
					case) the `Algorithm` class will pick up and act accordingly. Typically, this also means filtering a
 | 
				
			||||||
minor subset where possible to avoid over-iterating rooms.
 | 
					minor subset where possible to avoid over-iterating rooms.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -137,6 +144,13 @@ All filter conditions are considered "stable" by the consumers, meaning that the
 | 
				
			|||||||
expect a change in the condition unless the condition says it has changed. This is intentional to
 | 
					expect a change in the condition unless the condition says it has changed. This is intentional to
 | 
				
			||||||
maintain the caching behaviour described above.
 | 
					maintain the caching behaviour described above.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					One might ask why we don't just use prefilter conditions for everything, and the answer is one of slight
 | 
				
			||||||
 | 
					subtlety: in the cases of prefilters we are knowingly exposing the user to a workspace-style UX where
 | 
				
			||||||
 | 
					room notifications are self-contained within that workspace. Runtime filters tend to not want to affect
 | 
				
			||||||
 | 
					visible notification counts (as it doesn't want the room header to suddenly be confusing to the user as
 | 
				
			||||||
 | 
					they type), and occasionally UX like "found 2/12 rooms" is desirable. If prefiltering were used instead,
 | 
				
			||||||
 | 
					the notification counts would vary while the user was typing and "found 2/12" UX would not be possible.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## Class breakdowns
 | 
					## Class breakdowns
 | 
				
			||||||
 | 
					
 | 
				
			||||||
The `RoomListStore` is the major coordinator of various algorithm implementations, which take care
 | 
					The `RoomListStore` is the major coordinator of various algorithm implementations, which take care
 | 
				
			||||||
 | 
				
			|||||||
@ -40,6 +40,35 @@ limitations under the License.
 | 
				
			|||||||
        word-break: break-word;
 | 
					        word-break: break-word;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .mx_RoomPreviewBar_reason {
 | 
				
			||||||
 | 
					        text-align: left;
 | 
				
			||||||
 | 
					        background-color: $primary-bg-color;
 | 
				
			||||||
 | 
					        border: 1px solid $invite-reason-border-color;
 | 
				
			||||||
 | 
					        border-radius: 10px;
 | 
				
			||||||
 | 
					        padding: 0 16px 12px 16px;
 | 
				
			||||||
 | 
					        margin: 5px 0 20px 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        div {
 | 
				
			||||||
 | 
					            pointer-events: none;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        .mx_EventTile_msgOption {
 | 
				
			||||||
 | 
					            display: none;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        .mx_MatrixChat_useCompactLayout & {
 | 
				
			||||||
 | 
					            padding-top: 9px;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        &.mx_EventTilePreview_faded {
 | 
				
			||||||
 | 
					            cursor: pointer;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            .mx_SenderProfile, .mx_EventTile_avatar {
 | 
				
			||||||
 | 
					                opacity: 0.3;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    .mx_Spinner {
 | 
					    .mx_Spinner {
 | 
				
			||||||
        width: auto;
 | 
					        width: auto;
 | 
				
			||||||
        height: auto;
 | 
					        height: auto;
 | 
				
			||||||
 | 
				
			|||||||
@ -55,7 +55,7 @@ limitations under the License.
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    .mx_CallView_voice_holdText {
 | 
					    .mx_CallView_holdTransferContent {
 | 
				
			||||||
        padding-top: 10px;
 | 
					        padding-top: 10px;
 | 
				
			||||||
        padding-bottom: 25px;
 | 
					        padding-bottom: 25px;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@ -82,7 +82,7 @@ limitations under the License.
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.mx_CallView_voice_hold {
 | 
					.mx_CallView_voice .mx_CallView_holdTransferContent {
 | 
				
			||||||
    // This masks the avatar image so when it's blurred, the edge is still crisp
 | 
					    // This masks the avatar image so when it's blurred, the edge is still crisp
 | 
				
			||||||
    .mx_CallView_voice_avatarContainer {
 | 
					    .mx_CallView_voice_avatarContainer {
 | 
				
			||||||
        border-radius: 2000px;
 | 
					        border-radius: 2000px;
 | 
				
			||||||
@ -91,7 +91,7 @@ limitations under the License.
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.mx_CallView_voice_holdText {
 | 
					.mx_CallView_holdTransferContent {
 | 
				
			||||||
    height: 20px;
 | 
					    height: 20px;
 | 
				
			||||||
    padding-top: 20px;
 | 
					    padding-top: 20px;
 | 
				
			||||||
    padding-bottom: 15px;
 | 
					    padding-bottom: 15px;
 | 
				
			||||||
@ -142,7 +142,7 @@ limitations under the License.
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.mx_CallView_video_holdContent {
 | 
					.mx_CallView_video .mx_CallView_holdTransferContent {
 | 
				
			||||||
    position: absolute;
 | 
					    position: absolute;
 | 
				
			||||||
    top: 50%;
 | 
					    top: 50%;
 | 
				
			||||||
    left: 50%;
 | 
					    left: 50%;
 | 
				
			||||||
 | 
				
			|||||||
@ -209,6 +209,8 @@ $message-body-panel-fg-color: $primary-fg-color;
 | 
				
			|||||||
// Appearance tab colors
 | 
					// Appearance tab colors
 | 
				
			||||||
$appearance-tab-border-color: $room-highlight-color;
 | 
					$appearance-tab-border-color: $room-highlight-color;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					$invite-reason-border-color: $room-highlight-color;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// blur amounts for left left panel (only for element theme, used in _mods.scss)
 | 
					// blur amounts for left left panel (only for element theme, used in _mods.scss)
 | 
				
			||||||
$roomlist-background-blur-amount: 60px;
 | 
					$roomlist-background-blur-amount: 60px;
 | 
				
			||||||
$groupFilterPanel-background-blur-amount: 30px;
 | 
					$groupFilterPanel-background-blur-amount: 30px;
 | 
				
			||||||
 | 
				
			|||||||
@ -204,6 +204,8 @@ $message-body-panel-fg-color: $primary-fg-color;
 | 
				
			|||||||
// Appearance tab colors
 | 
					// Appearance tab colors
 | 
				
			||||||
$appearance-tab-border-color: $room-highlight-color;
 | 
					$appearance-tab-border-color: $room-highlight-color;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					$invite-reason-border-color: $room-highlight-color;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
$composer-shadow-color: tranparent;
 | 
					$composer-shadow-color: tranparent;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// ***** Mixins! *****
 | 
					// ***** Mixins! *****
 | 
				
			||||||
 | 
				
			|||||||
@ -333,6 +333,8 @@ $message-body-panel-fg-color: $muted-fg-color;
 | 
				
			|||||||
// FontSlider colors
 | 
					// FontSlider colors
 | 
				
			||||||
$appearance-tab-border-color: $input-darker-bg-color;
 | 
					$appearance-tab-border-color: $input-darker-bg-color;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					$invite-reason-border-color: $input-darker-bg-color;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
$composer-shadow-color: tranparent;
 | 
					$composer-shadow-color: tranparent;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// ***** Mixins! *****
 | 
					// ***** Mixins! *****
 | 
				
			||||||
 | 
				
			|||||||
@ -331,6 +331,8 @@ $message-body-panel-fg-color: $muted-fg-color;
 | 
				
			|||||||
// FontSlider colors
 | 
					// FontSlider colors
 | 
				
			||||||
$appearance-tab-border-color: $input-darker-bg-color;
 | 
					$appearance-tab-border-color: $input-darker-bg-color;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					$invite-reason-border-color: $input-darker-bg-color;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// blur amounts for left left panel (only for element theme, used in _mods.scss)
 | 
					// blur amounts for left left panel (only for element theme, used in _mods.scss)
 | 
				
			||||||
$roomlist-background-blur-amount: 40px;
 | 
					$roomlist-background-blur-amount: 40px;
 | 
				
			||||||
$groupFilterPanel-background-blur-amount: 20px;
 | 
					$groupFilterPanel-background-blur-amount: 20px;
 | 
				
			||||||
 | 
				
			|||||||
@ -154,6 +154,9 @@ function getRemoteAudioElement(): HTMLAudioElement {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
export default class CallHandler {
 | 
					export default class CallHandler {
 | 
				
			||||||
    private calls = new Map<string, MatrixCall>(); // roomId -> call
 | 
					    private calls = new Map<string, MatrixCall>(); // roomId -> call
 | 
				
			||||||
 | 
					    // Calls started as an attended transfer, ie. with the intention of transferring another
 | 
				
			||||||
 | 
					    // call with a different party to this one.
 | 
				
			||||||
 | 
					    private transferees = new Map<string, MatrixCall>(); // callId (target) -> call (transferee)
 | 
				
			||||||
    private audioPromises = new Map<AudioID, Promise<void>>();
 | 
					    private audioPromises = new Map<AudioID, Promise<void>>();
 | 
				
			||||||
    private dispatcherRef: string = null;
 | 
					    private dispatcherRef: string = null;
 | 
				
			||||||
    private supportsPstnProtocol = null;
 | 
					    private supportsPstnProtocol = null;
 | 
				
			||||||
@ -325,6 +328,10 @@ export default class CallHandler {
 | 
				
			|||||||
        return callsNotInThatRoom;
 | 
					        return callsNotInThatRoom;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    getTransfereeForCallId(callId: string): MatrixCall {
 | 
				
			||||||
 | 
					        return this.transferees[callId];
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    play(audioId: AudioID) {
 | 
					    play(audioId: AudioID) {
 | 
				
			||||||
        // TODO: Attach an invisible element for this instead
 | 
					        // TODO: Attach an invisible element for this instead
 | 
				
			||||||
        // which listens?
 | 
					        // which listens?
 | 
				
			||||||
@ -622,6 +629,7 @@ export default class CallHandler {
 | 
				
			|||||||
    private async placeCall(
 | 
					    private async placeCall(
 | 
				
			||||||
        roomId: string, type: PlaceCallType,
 | 
					        roomId: string, type: PlaceCallType,
 | 
				
			||||||
        localElement: HTMLVideoElement, remoteElement: HTMLVideoElement,
 | 
					        localElement: HTMLVideoElement, remoteElement: HTMLVideoElement,
 | 
				
			||||||
 | 
					        transferee: MatrixCall,
 | 
				
			||||||
    ) {
 | 
					    ) {
 | 
				
			||||||
        Analytics.trackEvent('voip', 'placeCall', 'type', type);
 | 
					        Analytics.trackEvent('voip', 'placeCall', 'type', type);
 | 
				
			||||||
        CountlyAnalytics.instance.trackStartCall(roomId, type === PlaceCallType.Video, false);
 | 
					        CountlyAnalytics.instance.trackStartCall(roomId, type === PlaceCallType.Video, false);
 | 
				
			||||||
@ -634,6 +642,9 @@ export default class CallHandler {
 | 
				
			|||||||
        const call = createNewMatrixCall(MatrixClientPeg.get(), mappedRoomId);
 | 
					        const call = createNewMatrixCall(MatrixClientPeg.get(), mappedRoomId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        this.calls.set(roomId, call);
 | 
					        this.calls.set(roomId, call);
 | 
				
			||||||
 | 
					        if (transferee) {
 | 
				
			||||||
 | 
					            this.transferees[call.callId] = transferee;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        this.setCallListeners(call);
 | 
					        this.setCallListeners(call);
 | 
				
			||||||
        this.setCallAudioElement(call);
 | 
					        this.setCallAudioElement(call);
 | 
				
			||||||
@ -723,7 +734,10 @@ export default class CallHandler {
 | 
				
			|||||||
                    } else if (members.length === 2) {
 | 
					                    } else if (members.length === 2) {
 | 
				
			||||||
                        console.info(`Place ${payload.type} call in ${payload.room_id}`);
 | 
					                        console.info(`Place ${payload.type} call in ${payload.room_id}`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                        this.placeCall(payload.room_id, payload.type, payload.local_element, payload.remote_element);
 | 
					                        this.placeCall(
 | 
				
			||||||
 | 
					                            payload.room_id, payload.type, payload.local_element, payload.remote_element,
 | 
				
			||||||
 | 
					                            payload.transferee,
 | 
				
			||||||
 | 
					                        );
 | 
				
			||||||
                    } else { // > 2
 | 
					                    } else { // > 2
 | 
				
			||||||
                        dis.dispatch({
 | 
					                        dis.dispatch({
 | 
				
			||||||
                            action: "place_conference_call",
 | 
					                            action: "place_conference_call",
 | 
				
			||||||
 | 
				
			|||||||
@ -161,27 +161,27 @@ const messageComposerBindings = (): KeyBinding<MessageComposerAction>[] => {
 | 
				
			|||||||
const autocompleteBindings = (): KeyBinding<AutocompleteAction>[] => {
 | 
					const autocompleteBindings = (): KeyBinding<AutocompleteAction>[] => {
 | 
				
			||||||
    return [
 | 
					    return [
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            action: AutocompleteAction.ApplySelection,
 | 
					            action: AutocompleteAction.CompleteOrNextSelection,
 | 
				
			||||||
            keyCombo: {
 | 
					            keyCombo: {
 | 
				
			||||||
                key: Key.TAB,
 | 
					                key: Key.TAB,
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            action: AutocompleteAction.ApplySelection,
 | 
					            action: AutocompleteAction.CompleteOrNextSelection,
 | 
				
			||||||
            keyCombo: {
 | 
					            keyCombo: {
 | 
				
			||||||
                key: Key.TAB,
 | 
					                key: Key.TAB,
 | 
				
			||||||
                ctrlKey: true,
 | 
					                ctrlKey: true,
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            action: AutocompleteAction.ApplySelection,
 | 
					            action: AutocompleteAction.CompleteOrPrevSelection,
 | 
				
			||||||
            keyCombo: {
 | 
					            keyCombo: {
 | 
				
			||||||
                key: Key.TAB,
 | 
					                key: Key.TAB,
 | 
				
			||||||
                shiftKey: true,
 | 
					                shiftKey: true,
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            action: AutocompleteAction.ApplySelection,
 | 
					            action: AutocompleteAction.CompleteOrPrevSelection,
 | 
				
			||||||
            keyCombo: {
 | 
					            keyCombo: {
 | 
				
			||||||
                key: Key.TAB,
 | 
					                key: Key.TAB,
 | 
				
			||||||
                ctrlKey: true,
 | 
					                ctrlKey: true,
 | 
				
			||||||
 | 
				
			|||||||
@ -52,14 +52,19 @@ export enum MessageComposerAction {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
/** Actions for text editing autocompletion */
 | 
					/** Actions for text editing autocompletion */
 | 
				
			||||||
export enum AutocompleteAction {
 | 
					export enum AutocompleteAction {
 | 
				
			||||||
    /** Apply the current autocomplete selection */
 | 
					    /**
 | 
				
			||||||
    ApplySelection = 'ApplySelection',
 | 
					     * Select previous selection or, if the autocompletion window is not shown, open the window and select the first
 | 
				
			||||||
    /** Cancel autocompletion */
 | 
					     * selection.
 | 
				
			||||||
    Cancel = 'Cancel',
 | 
					     */
 | 
				
			||||||
 | 
					    CompleteOrPrevSelection = 'ApplySelection',
 | 
				
			||||||
 | 
					    /** Select next selection or, if the autocompletion window is not shown, open it and select the first selection */
 | 
				
			||||||
 | 
					    CompleteOrNextSelection = 'CompleteOrNextSelection',
 | 
				
			||||||
    /** Move to the previous autocomplete selection */
 | 
					    /** Move to the previous autocomplete selection */
 | 
				
			||||||
    PrevSelection = 'PrevSelection',
 | 
					    PrevSelection = 'PrevSelection',
 | 
				
			||||||
    /** Move to the next autocomplete selection */
 | 
					    /** Move to the next autocomplete selection */
 | 
				
			||||||
    NextSelection = 'NextSelection',
 | 
					    NextSelection = 'NextSelection',
 | 
				
			||||||
 | 
					    /** Close the autocompletion window */
 | 
				
			||||||
 | 
					    Cancel = 'Cancel',
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/** Actions for the room list sidebar */
 | 
					/** Actions for the room list sidebar */
 | 
				
			||||||
 | 
				
			|||||||
@ -23,7 +23,6 @@ interface IOptions<T extends {}> {
 | 
				
			|||||||
    keys: Array<string | keyof T>;
 | 
					    keys: Array<string | keyof T>;
 | 
				
			||||||
    funcs?: Array<(T) => string>;
 | 
					    funcs?: Array<(T) => string>;
 | 
				
			||||||
    shouldMatchWordsOnly?: boolean;
 | 
					    shouldMatchWordsOnly?: boolean;
 | 
				
			||||||
    shouldMatchPrefix?: boolean;
 | 
					 | 
				
			||||||
    // whether to apply unhomoglyph and strip diacritics to fuzz up the search. Defaults to true
 | 
					    // whether to apply unhomoglyph and strip diacritics to fuzz up the search. Defaults to true
 | 
				
			||||||
    fuzzy?: boolean;
 | 
					    fuzzy?: boolean;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@ -56,12 +55,6 @@ export default class QueryMatcher<T extends Object> {
 | 
				
			|||||||
        if (this._options.shouldMatchWordsOnly === undefined) {
 | 
					        if (this._options.shouldMatchWordsOnly === undefined) {
 | 
				
			||||||
            this._options.shouldMatchWordsOnly = true;
 | 
					            this._options.shouldMatchWordsOnly = true;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					 | 
				
			||||||
        // By default, match anywhere in the string being searched. If enabled, only return
 | 
					 | 
				
			||||||
        // matches that are prefixed with the query.
 | 
					 | 
				
			||||||
        if (this._options.shouldMatchPrefix === undefined) {
 | 
					 | 
				
			||||||
            this._options.shouldMatchPrefix = false;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    setObjects(objects: T[]) {
 | 
					    setObjects(objects: T[]) {
 | 
				
			||||||
@ -112,7 +105,7 @@ export default class QueryMatcher<T extends Object> {
 | 
				
			|||||||
                resultKey = resultKey.replace(/[^\w]/g, '');
 | 
					                resultKey = resultKey.replace(/[^\w]/g, '');
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            const index = resultKey.indexOf(query);
 | 
					            const index = resultKey.indexOf(query);
 | 
				
			||||||
            if (index !== -1 && (!this._options.shouldMatchPrefix || index === 0)) {
 | 
					            if (index !== -1) {
 | 
				
			||||||
                matches.push(
 | 
					                matches.push(
 | 
				
			||||||
                    ...candidates.map((candidate) => ({index, ...candidate})),
 | 
					                    ...candidates.map((candidate) => ({index, ...candidate})),
 | 
				
			||||||
                );
 | 
					                );
 | 
				
			||||||
 | 
				
			|||||||
@ -56,7 +56,6 @@ export default class UserProvider extends AutocompleteProvider {
 | 
				
			|||||||
        this.matcher = new QueryMatcher([], {
 | 
					        this.matcher = new QueryMatcher([], {
 | 
				
			||||||
            keys: ['name'],
 | 
					            keys: ['name'],
 | 
				
			||||||
            funcs: [obj => obj.userId.slice(1)], // index by user id minus the leading '@'
 | 
					            funcs: [obj => obj.userId.slice(1)], // index by user id minus the leading '@'
 | 
				
			||||||
            shouldMatchPrefix: true,
 | 
					 | 
				
			||||||
            shouldMatchWordsOnly: false,
 | 
					            shouldMatchWordsOnly: false,
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -981,7 +981,7 @@ export default class GroupView extends React.Component {
 | 
				
			|||||||
                    <Spinner />
 | 
					                    <Spinner />
 | 
				
			||||||
                </div>;
 | 
					                </div>;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            const httpInviterAvatar = this.state.inviterProfile
 | 
					            const httpInviterAvatar = this.state.inviterProfile && this.state.inviterProfile.avatarUrl
 | 
				
			||||||
                ? mediaFromMxc(this.state.inviterProfile.avatarUrl).getSquareThumbnailHttp(36)
 | 
					                ? mediaFromMxc(this.state.inviterProfile.avatarUrl).getSquareThumbnailHttp(36)
 | 
				
			||||||
                : null;
 | 
					                : null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -34,7 +34,6 @@ import { UPDATE_EVENT } from "../../stores/AsyncStore";
 | 
				
			|||||||
import ResizeNotifier from "../../utils/ResizeNotifier";
 | 
					import ResizeNotifier from "../../utils/ResizeNotifier";
 | 
				
			||||||
import SettingsStore from "../../settings/SettingsStore";
 | 
					import SettingsStore from "../../settings/SettingsStore";
 | 
				
			||||||
import RoomListStore, { LISTS_UPDATE_EVENT } from "../../stores/room-list/RoomListStore";
 | 
					import RoomListStore, { LISTS_UPDATE_EVENT } from "../../stores/room-list/RoomListStore";
 | 
				
			||||||
import {Key} from "../../Keyboard";
 | 
					 | 
				
			||||||
import IndicatorScrollbar from "../structures/IndicatorScrollbar";
 | 
					import IndicatorScrollbar from "../structures/IndicatorScrollbar";
 | 
				
			||||||
import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton";
 | 
					import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton";
 | 
				
			||||||
import { OwnProfileStore } from "../../stores/OwnProfileStore";
 | 
					import { OwnProfileStore } from "../../stores/OwnProfileStore";
 | 
				
			||||||
@ -43,6 +42,7 @@ import LeftPanelWidget from "./LeftPanelWidget";
 | 
				
			|||||||
import {replaceableComponent} from "../../utils/replaceableComponent";
 | 
					import {replaceableComponent} from "../../utils/replaceableComponent";
 | 
				
			||||||
import {mediaFromMxc} from "../../customisations/Media";
 | 
					import {mediaFromMxc} from "../../customisations/Media";
 | 
				
			||||||
import SpaceStore, {UPDATE_SELECTED_SPACE} from "../../stores/SpaceStore";
 | 
					import SpaceStore, {UPDATE_SELECTED_SPACE} from "../../stores/SpaceStore";
 | 
				
			||||||
 | 
					import { getKeyBindingsManager, RoomListAction } from "../../KeyBindingsManager";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
interface IProps {
 | 
					interface IProps {
 | 
				
			||||||
    isMinimized: boolean;
 | 
					    isMinimized: boolean;
 | 
				
			||||||
@ -297,17 +297,18 @@ export default class LeftPanel extends React.Component<IProps, IState> {
 | 
				
			|||||||
    private onKeyDown = (ev: React.KeyboardEvent) => {
 | 
					    private onKeyDown = (ev: React.KeyboardEvent) => {
 | 
				
			||||||
        if (!this.focusedElement) return;
 | 
					        if (!this.focusedElement) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        switch (ev.key) {
 | 
					        const action = getKeyBindingsManager().getRoomListAction(ev);
 | 
				
			||||||
            case Key.ARROW_UP:
 | 
					        switch (action) {
 | 
				
			||||||
            case Key.ARROW_DOWN:
 | 
					            case RoomListAction.NextRoom:
 | 
				
			||||||
 | 
					            case RoomListAction.PrevRoom:
 | 
				
			||||||
                ev.stopPropagation();
 | 
					                ev.stopPropagation();
 | 
				
			||||||
                ev.preventDefault();
 | 
					                ev.preventDefault();
 | 
				
			||||||
                this.onMoveFocus(ev.key === Key.ARROW_UP);
 | 
					                this.onMoveFocus(action === RoomListAction.PrevRoom);
 | 
				
			||||||
                break;
 | 
					                break;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private onEnter = () => {
 | 
					    private selectRoom = () => {
 | 
				
			||||||
        const firstRoom = this.listContainerRef.current.querySelector<HTMLDivElement>(".mx_RoomTile");
 | 
					        const firstRoom = this.listContainerRef.current.querySelector<HTMLDivElement>(".mx_RoomTile");
 | 
				
			||||||
        if (firstRoom) {
 | 
					        if (firstRoom) {
 | 
				
			||||||
            firstRoom.click();
 | 
					            firstRoom.click();
 | 
				
			||||||
@ -388,8 +389,8 @@ export default class LeftPanel extends React.Component<IProps, IState> {
 | 
				
			|||||||
            >
 | 
					            >
 | 
				
			||||||
                <RoomSearch
 | 
					                <RoomSearch
 | 
				
			||||||
                    isMinimized={this.props.isMinimized}
 | 
					                    isMinimized={this.props.isMinimized}
 | 
				
			||||||
                    onVerticalArrow={this.onKeyDown}
 | 
					                    onKeyDown={this.onKeyDown}
 | 
				
			||||||
                    onEnter={this.onEnter}
 | 
					                    onSelectRoom={this.selectRoom}
 | 
				
			||||||
                />
 | 
					                />
 | 
				
			||||||
                <AccessibleTooltipButton
 | 
					                <AccessibleTooltipButton
 | 
				
			||||||
                    className={classNames("mx_LeftPanel_exploreButton", {
 | 
					                    className={classNames("mx_LeftPanel_exploreButton", {
 | 
				
			||||||
 | 
				
			|||||||
@ -444,6 +444,7 @@ class LoggedInView extends React.Component<IProps, IState> {
 | 
				
			|||||||
            case RoomAction.RoomScrollDown:
 | 
					            case RoomAction.RoomScrollDown:
 | 
				
			||||||
            case RoomAction.JumpToFirstMessage:
 | 
					            case RoomAction.JumpToFirstMessage:
 | 
				
			||||||
            case RoomAction.JumpToLatestMessage:
 | 
					            case RoomAction.JumpToLatestMessage:
 | 
				
			||||||
 | 
					                // pass the event down to the scroll panel
 | 
				
			||||||
                this._onScrollKeyPressed(ev);
 | 
					                this._onScrollKeyPressed(ev);
 | 
				
			||||||
                handled = true;
 | 
					                handled = true;
 | 
				
			||||||
                break;
 | 
					                break;
 | 
				
			||||||
 | 
				
			|||||||
@ -30,8 +30,11 @@ import SpaceStore, {UPDATE_SELECTED_SPACE} from "../../stores/SpaceStore";
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
interface IProps {
 | 
					interface IProps {
 | 
				
			||||||
    isMinimized: boolean;
 | 
					    isMinimized: boolean;
 | 
				
			||||||
    onVerticalArrow(ev: React.KeyboardEvent): void;
 | 
					    onKeyDown(ev: React.KeyboardEvent): void;
 | 
				
			||||||
    onEnter(ev: React.KeyboardEvent): boolean;
 | 
					    /**
 | 
				
			||||||
 | 
					     * @returns true if a room has been selected and the search field should be cleared
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    onSelectRoom(): boolean;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
interface IState {
 | 
					interface IState {
 | 
				
			||||||
@ -120,10 +123,11 @@ export default class RoomSearch extends React.PureComponent<IProps, IState> {
 | 
				
			|||||||
                break;
 | 
					                break;
 | 
				
			||||||
            case RoomListAction.NextRoom:
 | 
					            case RoomListAction.NextRoom:
 | 
				
			||||||
            case RoomListAction.PrevRoom:
 | 
					            case RoomListAction.PrevRoom:
 | 
				
			||||||
                this.props.onVerticalArrow(ev);
 | 
					                // we don't handle these actions here put pass the event on to the interested party (LeftPanel)
 | 
				
			||||||
 | 
					                this.props.onKeyDown(ev);
 | 
				
			||||||
                break;
 | 
					                break;
 | 
				
			||||||
            case RoomListAction.SelectRoom: {
 | 
					            case RoomListAction.SelectRoom: {
 | 
				
			||||||
                const shouldClear = this.props.onEnter(ev);
 | 
					                const shouldClear = this.props.onSelectRoom();
 | 
				
			||||||
                if (shouldClear) {
 | 
					                if (shouldClear) {
 | 
				
			||||||
                    // wrap in set immediate to delay it so that we don't clear the filter & then change room
 | 
					                    // wrap in set immediate to delay it so that we don't clear the filter & then change room
 | 
				
			||||||
                    setImmediate(() => {
 | 
					                    setImmediate(() => {
 | 
				
			||||||
 | 
				
			|||||||
@ -16,10 +16,10 @@ limitations under the License.
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
import React, {createRef} from "react";
 | 
					import React, {createRef} from "react";
 | 
				
			||||||
import PropTypes from 'prop-types';
 | 
					import PropTypes from 'prop-types';
 | 
				
			||||||
import { Key } from '../../Keyboard';
 | 
					 | 
				
			||||||
import Timer from '../../utils/Timer';
 | 
					import Timer from '../../utils/Timer';
 | 
				
			||||||
import AutoHideScrollbar from "./AutoHideScrollbar";
 | 
					import AutoHideScrollbar from "./AutoHideScrollbar";
 | 
				
			||||||
import {replaceableComponent} from "../../utils/replaceableComponent";
 | 
					import {replaceableComponent} from "../../utils/replaceableComponent";
 | 
				
			||||||
 | 
					import {getKeyBindingsManager, RoomAction} from "../../KeyBindingsManager";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const DEBUG_SCROLL = false;
 | 
					const DEBUG_SCROLL = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -539,34 +539,24 @@ export default class ScrollPanel extends React.Component {
 | 
				
			|||||||
     * @param {object} ev the keyboard event
 | 
					     * @param {object} ev the keyboard event
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    handleScrollKey = ev => {
 | 
					    handleScrollKey = ev => {
 | 
				
			||||||
        let isScrolling = false;
 | 
						let isScrolling = false;
 | 
				
			||||||
        switch (ev.key) {
 | 
					        const roomAction = getKeyBindingsManager().getRoomAction(ev);
 | 
				
			||||||
            case Key.PAGE_UP:
 | 
					        switch (roomAction) {
 | 
				
			||||||
                if (!ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey) {
 | 
					            case RoomAction.ScrollUp:
 | 
				
			||||||
                    isScrolling = true;
 | 
					                this.scrollRelative(-1);
 | 
				
			||||||
                    this.scrollRelative(-1);
 | 
							isScrolling = true;
 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
                break;
 | 
					                break;
 | 
				
			||||||
 | 
					            case RoomAction.RoomScrollDown:
 | 
				
			||||||
            case Key.PAGE_DOWN:
 | 
					                this.scrollRelative(1);
 | 
				
			||||||
                if (!ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey) {
 | 
							isScrolling = true;
 | 
				
			||||||
                    isScrolling = true;
 | 
					 | 
				
			||||||
                    this.scrollRelative(1);
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
                break;
 | 
					                break;
 | 
				
			||||||
 | 
					            case RoomAction.JumpToFirstMessage:
 | 
				
			||||||
            case Key.HOME:
 | 
					                this.scrollToTop();
 | 
				
			||||||
                if (ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey) {
 | 
							isScrolling = true;
 | 
				
			||||||
                    isScrolling = true;
 | 
					 | 
				
			||||||
                    this.scrollToTop();
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
                break;
 | 
					                break;
 | 
				
			||||||
 | 
					            case RoomAction.JumpToLatestMessage:
 | 
				
			||||||
            case Key.END:
 | 
					                this.scrollToBottom();
 | 
				
			||||||
                if (ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey) {
 | 
							isScrolling = true;
 | 
				
			||||||
                    isScrolling = true;
 | 
					 | 
				
			||||||
                    this.scrollToBottom();
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
                break;
 | 
					                break;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        if (isScrolling && this.props.onUserScroll) {
 | 
					        if (isScrolling && this.props.onUserScroll) {
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										938
									
								
								src/components/structures/ScrollPanel.js.orig
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										938
									
								
								src/components/structures/ScrollPanel.js.orig
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,938 @@
 | 
				
			|||||||
 | 
					/*
 | 
				
			||||||
 | 
					Copyright 2015, 2016 OpenMarket Ltd
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					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 React, {createRef} from "react";
 | 
				
			||||||
 | 
					import PropTypes from 'prop-types';
 | 
				
			||||||
 | 
					import Timer from '../../utils/Timer';
 | 
				
			||||||
 | 
					import AutoHideScrollbar from "./AutoHideScrollbar";
 | 
				
			||||||
 | 
					import {replaceableComponent} from "../../utils/replaceableComponent";
 | 
				
			||||||
 | 
					import {getKeyBindingsManager, RoomAction} from "../../KeyBindingsManager";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const DEBUG_SCROLL = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// The amount of extra scroll distance to allow prior to unfilling.
 | 
				
			||||||
 | 
					// See _getExcessHeight.
 | 
				
			||||||
 | 
					const UNPAGINATION_PADDING = 6000;
 | 
				
			||||||
 | 
					// The number of milliseconds to debounce calls to onUnfillRequest, to prevent
 | 
				
			||||||
 | 
					// many scroll events causing many unfilling requests.
 | 
				
			||||||
 | 
					const UNFILL_REQUEST_DEBOUNCE_MS = 200;
 | 
				
			||||||
 | 
					// _updateHeight makes the height a ceiled multiple of this so we
 | 
				
			||||||
 | 
					// don't have to update the height too often. It also allows the user
 | 
				
			||||||
 | 
					// to scroll past the pagination spinner a bit so they don't feel blocked so
 | 
				
			||||||
 | 
					// much while the content loads.
 | 
				
			||||||
 | 
					const PAGE_SIZE = 400;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					let debuglog;
 | 
				
			||||||
 | 
					if (DEBUG_SCROLL) {
 | 
				
			||||||
 | 
					    // using bind means that we get to keep useful line numbers in the console
 | 
				
			||||||
 | 
					    debuglog = console.log.bind(console, "ScrollPanel debuglog:");
 | 
				
			||||||
 | 
					} else {
 | 
				
			||||||
 | 
					    debuglog = function() {};
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* This component implements an intelligent scrolling list.
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * It wraps a list of <li> children; when items are added to the start or end
 | 
				
			||||||
 | 
					 * of the list, the scroll position is updated so that the user still sees the
 | 
				
			||||||
 | 
					 * same position in the list.
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * It also provides a hook which allows parents to provide more list elements
 | 
				
			||||||
 | 
					 * when we get close to the start or end of the list.
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * Each child element should have a 'data-scroll-tokens'. This string of
 | 
				
			||||||
 | 
					 * comma-separated tokens may contain a single token or many, where many indicates
 | 
				
			||||||
 | 
					 * that the element contains elements that have scroll tokens themselves. The first
 | 
				
			||||||
 | 
					 * token in 'data-scroll-tokens' is used to serialise the scroll state, and returned
 | 
				
			||||||
 | 
					 * as the 'trackedScrollToken' attribute by getScrollState().
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * IMPORTANT: INDIVIDUAL TOKENS WITHIN 'data-scroll-tokens' MUST NOT CONTAIN COMMAS.
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * Some notes about the implementation:
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * The saved 'scrollState' can exist in one of two states:
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 *   - stuckAtBottom: (the default, and restored by resetScrollState): the
 | 
				
			||||||
 | 
					 *     viewport is scrolled down as far as it can be. When the children are
 | 
				
			||||||
 | 
					 *     updated, the scroll position will be updated to ensure it is still at
 | 
				
			||||||
 | 
					 *     the bottom.
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 *   - fixed, in which the viewport is conceptually tied at a specific scroll
 | 
				
			||||||
 | 
					 *     offset.  We don't save the absolute scroll offset, because that would be
 | 
				
			||||||
 | 
					 *     affected by window width, zoom level, amount of scrollback, etc. Instead
 | 
				
			||||||
 | 
					 *     we save an identifier for the last fully-visible message, and the number
 | 
				
			||||||
 | 
					 *     of pixels the window was scrolled below it - which is hopefully near
 | 
				
			||||||
 | 
					 *     enough.
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * The 'stickyBottom' property controls the behaviour when we reach the bottom
 | 
				
			||||||
 | 
					 * of the window (either through a user-initiated scroll, or by calling
 | 
				
			||||||
 | 
					 * scrollToBottom). If stickyBottom is enabled, the scrollState will enter
 | 
				
			||||||
 | 
					 * 'stuckAtBottom' state - ensuring that new additions cause the window to
 | 
				
			||||||
 | 
					 * scroll down further. If stickyBottom is disabled, we just save the scroll
 | 
				
			||||||
 | 
					 * offset as normal.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@replaceableComponent("structures.ScrollPanel")
 | 
				
			||||||
 | 
					export default class ScrollPanel extends React.Component {
 | 
				
			||||||
 | 
					    static propTypes = {
 | 
				
			||||||
 | 
					        /* stickyBottom: if set to true, then once the user hits the bottom of
 | 
				
			||||||
 | 
					         * the list, any new children added to the list will cause the list to
 | 
				
			||||||
 | 
					         * scroll down to show the new element, rather than preserving the
 | 
				
			||||||
 | 
					         * existing view.
 | 
				
			||||||
 | 
					         */
 | 
				
			||||||
 | 
					        stickyBottom: PropTypes.bool,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /* startAtBottom: if set to true, the view is assumed to start
 | 
				
			||||||
 | 
					         * scrolled to the bottom.
 | 
				
			||||||
 | 
					         * XXX: It's likely this is unnecessary and can be derived from
 | 
				
			||||||
 | 
					         * stickyBottom, but I'm adding an extra parameter to ensure
 | 
				
			||||||
 | 
					         * behaviour stays the same for other uses of ScrollPanel.
 | 
				
			||||||
 | 
					         * If so, let's remove this parameter down the line.
 | 
				
			||||||
 | 
					         */
 | 
				
			||||||
 | 
					        startAtBottom: PropTypes.bool,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /* onFillRequest(backwards): a callback which is called on scroll when
 | 
				
			||||||
 | 
					         * the user nears the start (backwards = true) or end (backwards =
 | 
				
			||||||
 | 
					         * false) of the list.
 | 
				
			||||||
 | 
					         *
 | 
				
			||||||
 | 
					         * This should return a promise; no more calls will be made until the
 | 
				
			||||||
 | 
					         * promise completes.
 | 
				
			||||||
 | 
					         *
 | 
				
			||||||
 | 
					         * The promise should resolve to true if there is more data to be
 | 
				
			||||||
 | 
					         * retrieved in this direction (in which case onFillRequest may be
 | 
				
			||||||
 | 
					         * called again immediately), or false if there is no more data in this
 | 
				
			||||||
 | 
					         * directon (at this time) - which will stop the pagination cycle until
 | 
				
			||||||
 | 
					         * the user scrolls again.
 | 
				
			||||||
 | 
					         */
 | 
				
			||||||
 | 
					        onFillRequest: PropTypes.func,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /* onUnfillRequest(backwards): a callback which is called on scroll when
 | 
				
			||||||
 | 
					         * there are children elements that are far out of view and could be removed
 | 
				
			||||||
 | 
					         * without causing pagination to occur.
 | 
				
			||||||
 | 
					         *
 | 
				
			||||||
 | 
					         * This function should accept a boolean, which is true to indicate the back/top
 | 
				
			||||||
 | 
					         * of the panel and false otherwise, and a scroll token, which refers to the
 | 
				
			||||||
 | 
					         * first element to remove if removing from the front/bottom, and last element
 | 
				
			||||||
 | 
					         * to remove if removing from the back/top.
 | 
				
			||||||
 | 
					         */
 | 
				
			||||||
 | 
					        onUnfillRequest: PropTypes.func,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /* onScroll: a callback which is called whenever any scroll happens.
 | 
				
			||||||
 | 
					         */
 | 
				
			||||||
 | 
					        onScroll: PropTypes.func,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /* onUserScroll: callback which is called when the user interacts with the room timeline
 | 
				
			||||||
 | 
					         */
 | 
				
			||||||
 | 
					        onUserScroll: PropTypes.func,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /* className: classnames to add to the top-level div
 | 
				
			||||||
 | 
					         */
 | 
				
			||||||
 | 
					        className: PropTypes.string,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /* style: styles to add to the top-level div
 | 
				
			||||||
 | 
					         */
 | 
				
			||||||
 | 
					        style: PropTypes.object,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /* resizeNotifier: ResizeNotifier to know when middle column has changed size
 | 
				
			||||||
 | 
					         */
 | 
				
			||||||
 | 
					        resizeNotifier: PropTypes.object,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /* fixedChildren: allows for children to be passed which are rendered outside
 | 
				
			||||||
 | 
					         * of the wrapper
 | 
				
			||||||
 | 
					         */
 | 
				
			||||||
 | 
					        fixedChildren: PropTypes.node,
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    static defaultProps = {
 | 
				
			||||||
 | 
					        stickyBottom: true,
 | 
				
			||||||
 | 
					        startAtBottom: true,
 | 
				
			||||||
 | 
					        onFillRequest: function(backwards) { return Promise.resolve(false); },
 | 
				
			||||||
 | 
					        onUnfillRequest: function(backwards, scrollToken) {},
 | 
				
			||||||
 | 
					        onScroll: function() {},
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    constructor(props) {
 | 
				
			||||||
 | 
					        super(props);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        this._pendingFillRequests = {b: null, f: null};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (this.props.resizeNotifier) {
 | 
				
			||||||
 | 
					            this.props.resizeNotifier.on("middlePanelResizedNoisy", this.onResize);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        this.resetScrollState();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        this._itemlist = createRef();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    componentDidMount() {
 | 
				
			||||||
 | 
					        this.checkScroll();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    componentDidUpdate() {
 | 
				
			||||||
 | 
					        // after adding event tiles, we may need to tweak the scroll (either to
 | 
				
			||||||
 | 
					        // keep at the bottom of the timeline, or to maintain the view after
 | 
				
			||||||
 | 
					        // adding events to the top).
 | 
				
			||||||
 | 
					        //
 | 
				
			||||||
 | 
					        // This will also re-check the fill state, in case the paginate was inadequate
 | 
				
			||||||
 | 
					        this.checkScroll();
 | 
				
			||||||
 | 
					        this.updatePreventShrinking();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    componentWillUnmount() {
 | 
				
			||||||
 | 
					        // set a boolean to say we've been unmounted, which any pending
 | 
				
			||||||
 | 
					        // promises can use to throw away their results.
 | 
				
			||||||
 | 
					        //
 | 
				
			||||||
 | 
					        // (We could use isMounted(), but facebook have deprecated that.)
 | 
				
			||||||
 | 
					        this.unmounted = true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (this.props.resizeNotifier) {
 | 
				
			||||||
 | 
					            this.props.resizeNotifier.removeListener("middlePanelResizedNoisy", this.onResize);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    onScroll = ev => {
 | 
				
			||||||
 | 
					        // skip scroll events caused by resizing
 | 
				
			||||||
 | 
					        if (this.props.resizeNotifier && this.props.resizeNotifier.isResizing) return;
 | 
				
			||||||
 | 
					        debuglog("onScroll", this._getScrollNode().scrollTop);
 | 
				
			||||||
 | 
					        this._scrollTimeout.restart();
 | 
				
			||||||
 | 
					        this._saveScrollState();
 | 
				
			||||||
 | 
					        this.updatePreventShrinking();
 | 
				
			||||||
 | 
					        this.props.onScroll(ev);
 | 
				
			||||||
 | 
					        this.checkFillState();
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    onResize = () => {
 | 
				
			||||||
 | 
					        debuglog("onResize");
 | 
				
			||||||
 | 
					        this.checkScroll();
 | 
				
			||||||
 | 
					        // update preventShrinkingState if present
 | 
				
			||||||
 | 
					        if (this.preventShrinkingState) {
 | 
				
			||||||
 | 
					            this.preventShrinking();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // after an update to the contents of the panel, check that the scroll is
 | 
				
			||||||
 | 
					    // where it ought to be, and set off pagination requests if necessary.
 | 
				
			||||||
 | 
					    checkScroll = () => {
 | 
				
			||||||
 | 
					        if (this.unmounted) {
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        this._restoreSavedScrollState();
 | 
				
			||||||
 | 
					        this.checkFillState();
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // return true if the content is fully scrolled down right now; else false.
 | 
				
			||||||
 | 
					    //
 | 
				
			||||||
 | 
					    // note that this is independent of the 'stuckAtBottom' state - it is simply
 | 
				
			||||||
 | 
					    // about whether the content is scrolled down right now, irrespective of
 | 
				
			||||||
 | 
					    // whether it will stay that way when the children update.
 | 
				
			||||||
 | 
					    isAtBottom = () => {
 | 
				
			||||||
 | 
					        const sn = this._getScrollNode();
 | 
				
			||||||
 | 
					        // fractional values (both too big and too small)
 | 
				
			||||||
 | 
					        // for scrollTop happen on certain browsers/platforms
 | 
				
			||||||
 | 
					        // when scrolled all the way down. E.g. Chrome 72 on debian.
 | 
				
			||||||
 | 
					        // so check difference <= 1;
 | 
				
			||||||
 | 
					        return Math.abs(sn.scrollHeight - (sn.scrollTop + sn.clientHeight)) <= 1;
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // returns the vertical height in the given direction that can be removed from
 | 
				
			||||||
 | 
					    // the content box (which has a height of scrollHeight, see checkFillState) without
 | 
				
			||||||
 | 
					    // pagination occuring.
 | 
				
			||||||
 | 
					    //
 | 
				
			||||||
 | 
					    // padding* = UNPAGINATION_PADDING
 | 
				
			||||||
 | 
					    //
 | 
				
			||||||
 | 
					    // ### Region determined as excess.
 | 
				
			||||||
 | 
					    //
 | 
				
			||||||
 | 
					    //   .---------.                        -              -
 | 
				
			||||||
 | 
					    //   |#########|                        |              |
 | 
				
			||||||
 | 
					    //   |#########|   -                    |  scrollTop   |
 | 
				
			||||||
 | 
					    //   |         |   | padding*           |              |
 | 
				
			||||||
 | 
					    //   |         |   |                    |              |
 | 
				
			||||||
 | 
					    // .-+---------+-. -  -                 |              |
 | 
				
			||||||
 | 
					    // : |         | :    |                 |              |
 | 
				
			||||||
 | 
					    // : |         | :    |  clientHeight   |              |
 | 
				
			||||||
 | 
					    // : |         | :    |                 |              |
 | 
				
			||||||
 | 
					    // .-+---------+-.    -                 -              |
 | 
				
			||||||
 | 
					    // | |         | |    |                                |
 | 
				
			||||||
 | 
					    // | |         | |    |  clientHeight                  | scrollHeight
 | 
				
			||||||
 | 
					    // | |         | |    |                                |
 | 
				
			||||||
 | 
					    // `-+---------+-'    -                                |
 | 
				
			||||||
 | 
					    // : |         | :    |                                |
 | 
				
			||||||
 | 
					    // : |         | :    |  clientHeight                  |
 | 
				
			||||||
 | 
					    // : |         | :    |                                |
 | 
				
			||||||
 | 
					    // `-+---------+-' -  -                                |
 | 
				
			||||||
 | 
					    //   |         |   | padding*                          |
 | 
				
			||||||
 | 
					    //   |         |   |                                   |
 | 
				
			||||||
 | 
					    //   |#########|   -                                   |
 | 
				
			||||||
 | 
					    //   |#########|                                       |
 | 
				
			||||||
 | 
					    //   `---------'                                       -
 | 
				
			||||||
 | 
					    _getExcessHeight(backwards) {
 | 
				
			||||||
 | 
					        const sn = this._getScrollNode();
 | 
				
			||||||
 | 
					        const contentHeight = this._getMessagesHeight();
 | 
				
			||||||
 | 
					        const listHeight = this._getListHeight();
 | 
				
			||||||
 | 
					        const clippedHeight = contentHeight - listHeight;
 | 
				
			||||||
 | 
					        const unclippedScrollTop = sn.scrollTop + clippedHeight;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (backwards) {
 | 
				
			||||||
 | 
					            return unclippedScrollTop - sn.clientHeight - UNPAGINATION_PADDING;
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            return contentHeight - (unclippedScrollTop + 2*sn.clientHeight) - UNPAGINATION_PADDING;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // check the scroll state and send out backfill requests if necessary.
 | 
				
			||||||
 | 
					    checkFillState = async (depth=0) => {
 | 
				
			||||||
 | 
					        if (this.unmounted) {
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const isFirstCall = depth === 0;
 | 
				
			||||||
 | 
					        const sn = this._getScrollNode();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // if there is less than a screenful of messages above or below the
 | 
				
			||||||
 | 
					        // viewport, try to get some more messages.
 | 
				
			||||||
 | 
					        //
 | 
				
			||||||
 | 
					        // scrollTop is the number of pixels between the top of the content and
 | 
				
			||||||
 | 
					        //     the top of the viewport.
 | 
				
			||||||
 | 
					        //
 | 
				
			||||||
 | 
					        // scrollHeight is the total height of the content.
 | 
				
			||||||
 | 
					        //
 | 
				
			||||||
 | 
					        // clientHeight is the height of the viewport (excluding borders,
 | 
				
			||||||
 | 
					        // margins, and scrollbars).
 | 
				
			||||||
 | 
					        //
 | 
				
			||||||
 | 
					        //
 | 
				
			||||||
 | 
					        //   .---------.          -                 -
 | 
				
			||||||
 | 
					        //   |         |          |  scrollTop      |
 | 
				
			||||||
 | 
					        // .-+---------+-.    -   -                 |
 | 
				
			||||||
 | 
					        // | |         | |    |                     |
 | 
				
			||||||
 | 
					        // | |         | |    |  clientHeight       | scrollHeight
 | 
				
			||||||
 | 
					        // | |         | |    |                     |
 | 
				
			||||||
 | 
					        // `-+---------+-'    -                     |
 | 
				
			||||||
 | 
					        //   |         |                            |
 | 
				
			||||||
 | 
					        //   |         |                            |
 | 
				
			||||||
 | 
					        //   `---------'                            -
 | 
				
			||||||
 | 
					        //
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // as filling is async and recursive,
 | 
				
			||||||
 | 
					        // don't allow more than 1 chain of calls concurrently
 | 
				
			||||||
 | 
					        // do make a note when a new request comes in while already running one,
 | 
				
			||||||
 | 
					        // so we can trigger a new chain of calls once done.
 | 
				
			||||||
 | 
					        if (isFirstCall) {
 | 
				
			||||||
 | 
					            if (this._isFilling) {
 | 
				
			||||||
 | 
					                debuglog("_isFilling: not entering while request is ongoing, marking for a subsequent request");
 | 
				
			||||||
 | 
					                this._fillRequestWhileRunning = true;
 | 
				
			||||||
 | 
					                return;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            debuglog("_isFilling: setting");
 | 
				
			||||||
 | 
					            this._isFilling = true;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const itemlist = this._itemlist.current;
 | 
				
			||||||
 | 
					        const firstTile = itemlist && itemlist.firstElementChild;
 | 
				
			||||||
 | 
					        const contentTop = firstTile && firstTile.offsetTop;
 | 
				
			||||||
 | 
					        const fillPromises = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // if scrollTop gets to 1 screen from the top of the first tile,
 | 
				
			||||||
 | 
					        // try backward filling
 | 
				
			||||||
 | 
					        if (!firstTile || (sn.scrollTop - contentTop) < sn.clientHeight) {
 | 
				
			||||||
 | 
					            // need to back-fill
 | 
				
			||||||
 | 
					            fillPromises.push(this._maybeFill(depth, true));
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        // if scrollTop gets to 2 screens from the end (so 1 screen below viewport),
 | 
				
			||||||
 | 
					        // try forward filling
 | 
				
			||||||
 | 
					        if ((sn.scrollHeight - sn.scrollTop) < sn.clientHeight * 2) {
 | 
				
			||||||
 | 
					            // need to forward-fill
 | 
				
			||||||
 | 
					            fillPromises.push(this._maybeFill(depth, false));
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (fillPromises.length) {
 | 
				
			||||||
 | 
					            try {
 | 
				
			||||||
 | 
					                await Promise.all(fillPromises);
 | 
				
			||||||
 | 
					            } catch (err) {
 | 
				
			||||||
 | 
					                console.error(err);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        if (isFirstCall) {
 | 
				
			||||||
 | 
					            debuglog("_isFilling: clearing");
 | 
				
			||||||
 | 
					            this._isFilling = false;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (this._fillRequestWhileRunning) {
 | 
				
			||||||
 | 
					            this._fillRequestWhileRunning = false;
 | 
				
			||||||
 | 
					            this.checkFillState();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // check if unfilling is possible and send an unfill request if necessary
 | 
				
			||||||
 | 
					    _checkUnfillState(backwards) {
 | 
				
			||||||
 | 
					        let excessHeight = this._getExcessHeight(backwards);
 | 
				
			||||||
 | 
					        if (excessHeight <= 0) {
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const origExcessHeight = excessHeight;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const tiles = this._itemlist.current.children;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // The scroll token of the first/last tile to be unpaginated
 | 
				
			||||||
 | 
					        let markerScrollToken = null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Subtract heights of tiles to simulate the tiles being unpaginated until the
 | 
				
			||||||
 | 
					        // excess height is less than the height of the next tile to subtract. This
 | 
				
			||||||
 | 
					        // prevents excessHeight becoming negative, which could lead to future
 | 
				
			||||||
 | 
					        // pagination.
 | 
				
			||||||
 | 
					        //
 | 
				
			||||||
 | 
					        // If backwards is true, we unpaginate (remove) tiles from the back (top).
 | 
				
			||||||
 | 
					        let tile;
 | 
				
			||||||
 | 
					        for (let i = 0; i < tiles.length; i++) {
 | 
				
			||||||
 | 
					            tile = tiles[backwards ? i : tiles.length - 1 - i];
 | 
				
			||||||
 | 
					            // Subtract height of tile as if it were unpaginated
 | 
				
			||||||
 | 
					            excessHeight -= tile.clientHeight;
 | 
				
			||||||
 | 
					            //If removing the tile would lead to future pagination, break before setting scroll token
 | 
				
			||||||
 | 
					            if (tile.clientHeight > excessHeight) {
 | 
				
			||||||
 | 
					                break;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            // The tile may not have a scroll token, so guard it
 | 
				
			||||||
 | 
					            if (tile.dataset.scrollTokens) {
 | 
				
			||||||
 | 
					                markerScrollToken = tile.dataset.scrollTokens.split(',')[0];
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (markerScrollToken) {
 | 
				
			||||||
 | 
					            // Use a debouncer to prevent multiple unfill calls in quick succession
 | 
				
			||||||
 | 
					            // This is to make the unfilling process less aggressive
 | 
				
			||||||
 | 
					            if (this._unfillDebouncer) {
 | 
				
			||||||
 | 
					                clearTimeout(this._unfillDebouncer);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            this._unfillDebouncer = setTimeout(() => {
 | 
				
			||||||
 | 
					                this._unfillDebouncer = null;
 | 
				
			||||||
 | 
					                debuglog("unfilling now", backwards, origExcessHeight);
 | 
				
			||||||
 | 
					                this.props.onUnfillRequest(backwards, markerScrollToken);
 | 
				
			||||||
 | 
					            }, UNFILL_REQUEST_DEBOUNCE_MS);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // check if there is already a pending fill request. If not, set one off.
 | 
				
			||||||
 | 
					    _maybeFill(depth, backwards) {
 | 
				
			||||||
 | 
					        const dir = backwards ? 'b' : 'f';
 | 
				
			||||||
 | 
					        if (this._pendingFillRequests[dir]) {
 | 
				
			||||||
 | 
					            debuglog("Already a "+dir+" fill in progress - not starting another");
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        debuglog("starting "+dir+" fill");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // onFillRequest can end up calling us recursively (via onScroll
 | 
				
			||||||
 | 
					        // events) so make sure we set this before firing off the call.
 | 
				
			||||||
 | 
					        this._pendingFillRequests[dir] = true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // wait 1ms before paginating, because otherwise
 | 
				
			||||||
 | 
					        // this will block the scroll event handler for +700ms
 | 
				
			||||||
 | 
					        // if messages are already cached in memory,
 | 
				
			||||||
 | 
					        // This would cause jumping to happen on Chrome/macOS.
 | 
				
			||||||
 | 
					        return new Promise(resolve => setTimeout(resolve, 1)).then(() => {
 | 
				
			||||||
 | 
					            return this.props.onFillRequest(backwards);
 | 
				
			||||||
 | 
					        }).finally(() => {
 | 
				
			||||||
 | 
					            this._pendingFillRequests[dir] = false;
 | 
				
			||||||
 | 
					        }).then((hasMoreResults) => {
 | 
				
			||||||
 | 
					            if (this.unmounted) {
 | 
				
			||||||
 | 
					                return;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            // Unpaginate once filling is complete
 | 
				
			||||||
 | 
					            this._checkUnfillState(!backwards);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            debuglog(""+dir+" fill complete; hasMoreResults:"+hasMoreResults);
 | 
				
			||||||
 | 
					            if (hasMoreResults) {
 | 
				
			||||||
 | 
					                // further pagination requests have been disabled until now, so
 | 
				
			||||||
 | 
					                // it's time to check the fill state again in case the pagination
 | 
				
			||||||
 | 
					                // was insufficient.
 | 
				
			||||||
 | 
					                return this.checkFillState(depth + 1);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /* get the current scroll state. This returns an object with the following
 | 
				
			||||||
 | 
					     * properties:
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * boolean stuckAtBottom: true if we are tracking the bottom of the
 | 
				
			||||||
 | 
					     *   scroll. false if we are tracking a particular child.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * string trackedScrollToken: undefined if stuckAtBottom is true; if it is
 | 
				
			||||||
 | 
					     *   false, the first token in data-scroll-tokens of the child which we are
 | 
				
			||||||
 | 
					     *   tracking.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * number bottomOffset: undefined if stuckAtBottom is true; if it is false,
 | 
				
			||||||
 | 
					     *   the number of pixels the bottom of the tracked child is above the
 | 
				
			||||||
 | 
					     *   bottom of the scroll panel.
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    getScrollState = () => this.scrollState;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /* reset the saved scroll state.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * This is useful if the list is being replaced, and you don't want to
 | 
				
			||||||
 | 
					     * preserve scroll even if new children happen to have the same scroll
 | 
				
			||||||
 | 
					     * tokens as old ones.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * This will cause the viewport to be scrolled down to the bottom on the
 | 
				
			||||||
 | 
					     * next update of the child list. This is different to scrollToBottom(),
 | 
				
			||||||
 | 
					     * which would save the current bottom-most child as the active one (so is
 | 
				
			||||||
 | 
					     * no use if no children exist yet, or if you are about to replace the
 | 
				
			||||||
 | 
					     * child list.)
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    resetScrollState = () => {
 | 
				
			||||||
 | 
					        this.scrollState = {
 | 
				
			||||||
 | 
					            stuckAtBottom: this.props.startAtBottom,
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					        this._bottomGrowth = 0;
 | 
				
			||||||
 | 
					        this._pages = 0;
 | 
				
			||||||
 | 
					        this._scrollTimeout = new Timer(100);
 | 
				
			||||||
 | 
					        this._heightUpdateInProgress = false;
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * jump to the top of the content.
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    scrollToTop = () => {
 | 
				
			||||||
 | 
					        this._getScrollNode().scrollTop = 0;
 | 
				
			||||||
 | 
					        this._saveScrollState();
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * jump to the bottom of the content.
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    scrollToBottom = () => {
 | 
				
			||||||
 | 
					        // the easiest way to make sure that the scroll state is correctly
 | 
				
			||||||
 | 
					        // saved is to do the scroll, then save the updated state. (Calculating
 | 
				
			||||||
 | 
					        // it ourselves is hard, and we can't rely on an onScroll callback
 | 
				
			||||||
 | 
					        // happening, since there may be no user-visible change here).
 | 
				
			||||||
 | 
					        const sn = this._getScrollNode();
 | 
				
			||||||
 | 
					        sn.scrollTop = sn.scrollHeight;
 | 
				
			||||||
 | 
					        this._saveScrollState();
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Page up/down.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @param {number} mult: -1 to page up, +1 to page down
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    scrollRelative = mult => {
 | 
				
			||||||
 | 
					        const scrollNode = this._getScrollNode();
 | 
				
			||||||
 | 
					        const delta = mult * scrollNode.clientHeight * 0.5;
 | 
				
			||||||
 | 
					        scrollNode.scrollBy(0, delta);
 | 
				
			||||||
 | 
					        this._saveScrollState();
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Scroll up/down in response to a scroll key
 | 
				
			||||||
 | 
					     * @param {object} ev the keyboard event
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    handleScrollKey = ev => {
 | 
				
			||||||
 | 
					<<<<<<< HEAD
 | 
				
			||||||
 | 
					        let isScrolling = false;
 | 
				
			||||||
 | 
					        switch (ev.key) {
 | 
				
			||||||
 | 
					            case Key.PAGE_UP:
 | 
				
			||||||
 | 
					                if (!ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey) {
 | 
				
			||||||
 | 
					                    isScrolling = true;
 | 
				
			||||||
 | 
					                    this.scrollRelative(-1);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                break;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            case Key.PAGE_DOWN:
 | 
				
			||||||
 | 
					                if (!ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey) {
 | 
				
			||||||
 | 
					                    isScrolling = true;
 | 
				
			||||||
 | 
					                    this.scrollRelative(1);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                break;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            case Key.HOME:
 | 
				
			||||||
 | 
					                if (ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey) {
 | 
				
			||||||
 | 
					                    isScrolling = true;
 | 
				
			||||||
 | 
					                    this.scrollToTop();
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                break;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            case Key.END:
 | 
				
			||||||
 | 
					                if (ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey) {
 | 
				
			||||||
 | 
					                    isScrolling = true;
 | 
				
			||||||
 | 
					                    this.scrollToBottom();
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					=======
 | 
				
			||||||
 | 
					        const roomAction = getKeyBindingsManager().getRoomAction(ev);
 | 
				
			||||||
 | 
					        switch (roomAction) {
 | 
				
			||||||
 | 
					            case RoomAction.ScrollUp:
 | 
				
			||||||
 | 
					                this.scrollRelative(-1);
 | 
				
			||||||
 | 
					                break;
 | 
				
			||||||
 | 
					            case RoomAction.RoomScrollDown:
 | 
				
			||||||
 | 
					                this.scrollRelative(1);
 | 
				
			||||||
 | 
					                break;
 | 
				
			||||||
 | 
					            case RoomAction.JumpToFirstMessage:
 | 
				
			||||||
 | 
					                this.scrollToTop();
 | 
				
			||||||
 | 
					                break;
 | 
				
			||||||
 | 
					            case RoomAction.JumpToLatestMessage:
 | 
				
			||||||
 | 
					                this.scrollToBottom();
 | 
				
			||||||
 | 
					>>>>>>> develop
 | 
				
			||||||
 | 
					                break;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        if (isScrolling && this.props.onUserScroll) {
 | 
				
			||||||
 | 
					            this.props.onUserScroll(ev);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /* Scroll the panel to bring the DOM node with the scroll token
 | 
				
			||||||
 | 
					     * `scrollToken` into view.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * offsetBase gives the reference point for the pixelOffset. 0 means the
 | 
				
			||||||
 | 
					     * top of the container, 1 means the bottom, and fractional values mean
 | 
				
			||||||
 | 
					     * somewhere in the middle. If omitted, it defaults to 0.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * pixelOffset gives the number of pixels *above* the offsetBase that the
 | 
				
			||||||
 | 
					     * node (specifically, the bottom of it) will be positioned. If omitted, it
 | 
				
			||||||
 | 
					     * defaults to 0.
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    scrollToToken = (scrollToken, pixelOffset, offsetBase) => {
 | 
				
			||||||
 | 
					        pixelOffset = pixelOffset || 0;
 | 
				
			||||||
 | 
					        offsetBase = offsetBase || 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // set the trackedScrollToken so we can get the node through _getTrackedNode
 | 
				
			||||||
 | 
					        this.scrollState = {
 | 
				
			||||||
 | 
					            stuckAtBottom: false,
 | 
				
			||||||
 | 
					            trackedScrollToken: scrollToken,
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					        const trackedNode = this._getTrackedNode();
 | 
				
			||||||
 | 
					        const scrollNode = this._getScrollNode();
 | 
				
			||||||
 | 
					        if (trackedNode) {
 | 
				
			||||||
 | 
					            // set the scrollTop to the position we want.
 | 
				
			||||||
 | 
					            // note though, that this might not succeed if the combination of offsetBase and pixelOffset
 | 
				
			||||||
 | 
					            // would position the trackedNode towards the top of the viewport.
 | 
				
			||||||
 | 
					            // This because when setting the scrollTop only 10 or so events might be loaded,
 | 
				
			||||||
 | 
					            // not giving enough content below the trackedNode to scroll downwards
 | 
				
			||||||
 | 
					            // enough so it ends up in the top of the viewport.
 | 
				
			||||||
 | 
					            debuglog("scrollToken: setting scrollTop", {offsetBase, pixelOffset, offsetTop: trackedNode.offsetTop});
 | 
				
			||||||
 | 
					            scrollNode.scrollTop = (trackedNode.offsetTop - (scrollNode.clientHeight * offsetBase)) + pixelOffset;
 | 
				
			||||||
 | 
					            this._saveScrollState();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    _saveScrollState() {
 | 
				
			||||||
 | 
					        if (this.props.stickyBottom && this.isAtBottom()) {
 | 
				
			||||||
 | 
					            this.scrollState = { stuckAtBottom: true };
 | 
				
			||||||
 | 
					            debuglog("saved stuckAtBottom state");
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const scrollNode = this._getScrollNode();
 | 
				
			||||||
 | 
					        const viewportBottom = scrollNode.scrollHeight - (scrollNode.scrollTop + scrollNode.clientHeight);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const itemlist = this._itemlist.current;
 | 
				
			||||||
 | 
					        const messages = itemlist.children;
 | 
				
			||||||
 | 
					        let node = null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // TODO: do a binary search here, as items are sorted by offsetTop
 | 
				
			||||||
 | 
					        // loop backwards, from bottom-most message (as that is the most common case)
 | 
				
			||||||
 | 
					        for (let i = messages.length-1; i >= 0; --i) {
 | 
				
			||||||
 | 
					            if (!messages[i].dataset.scrollTokens) {
 | 
				
			||||||
 | 
					                continue;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            node = messages[i];
 | 
				
			||||||
 | 
					            // break at the first message (coming from the bottom)
 | 
				
			||||||
 | 
					            // that has it's offsetTop above the bottom of the viewport.
 | 
				
			||||||
 | 
					            if (this._topFromBottom(node) > viewportBottom) {
 | 
				
			||||||
 | 
					                // Use this node as the scrollToken
 | 
				
			||||||
 | 
					                break;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (!node) {
 | 
				
			||||||
 | 
					            debuglog("unable to save scroll state: found no children in the viewport");
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        const scrollToken = node.dataset.scrollTokens.split(',')[0];
 | 
				
			||||||
 | 
					        debuglog("saving anchored scroll state to message", node && node.innerText, scrollToken);
 | 
				
			||||||
 | 
					        const bottomOffset = this._topFromBottom(node);
 | 
				
			||||||
 | 
					        this.scrollState = {
 | 
				
			||||||
 | 
					            stuckAtBottom: false,
 | 
				
			||||||
 | 
					            trackedNode: node,
 | 
				
			||||||
 | 
					            trackedScrollToken: scrollToken,
 | 
				
			||||||
 | 
					            bottomOffset: bottomOffset,
 | 
				
			||||||
 | 
					            pixelOffset: bottomOffset - viewportBottom, //needed for restoring the scroll position when coming back to the room
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async _restoreSavedScrollState() {
 | 
				
			||||||
 | 
					        const scrollState = this.scrollState;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (scrollState.stuckAtBottom) {
 | 
				
			||||||
 | 
					            const sn = this._getScrollNode();
 | 
				
			||||||
 | 
					            if (sn.scrollTop !== sn.scrollHeight) {
 | 
				
			||||||
 | 
					                sn.scrollTop = sn.scrollHeight;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        } else if (scrollState.trackedScrollToken) {
 | 
				
			||||||
 | 
					            const itemlist = this._itemlist.current;
 | 
				
			||||||
 | 
					            const trackedNode = this._getTrackedNode();
 | 
				
			||||||
 | 
					            if (trackedNode) {
 | 
				
			||||||
 | 
					                const newBottomOffset = this._topFromBottom(trackedNode);
 | 
				
			||||||
 | 
					                const bottomDiff = newBottomOffset - scrollState.bottomOffset;
 | 
				
			||||||
 | 
					                this._bottomGrowth += bottomDiff;
 | 
				
			||||||
 | 
					                scrollState.bottomOffset = newBottomOffset;
 | 
				
			||||||
 | 
					                const newHeight = `${this._getListHeight()}px`;
 | 
				
			||||||
 | 
					                if (itemlist.style.height !== newHeight) {
 | 
				
			||||||
 | 
					                    itemlist.style.height = newHeight;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                debuglog("balancing height because messages below viewport grew by", bottomDiff);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        if (!this._heightUpdateInProgress) {
 | 
				
			||||||
 | 
					            this._heightUpdateInProgress = true;
 | 
				
			||||||
 | 
					            try {
 | 
				
			||||||
 | 
					                await this._updateHeight();
 | 
				
			||||||
 | 
					            } finally {
 | 
				
			||||||
 | 
					                this._heightUpdateInProgress = false;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            debuglog("not updating height because request already in progress");
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // need a better name that also indicates this will change scrollTop? Rebalance height? Reveal content?
 | 
				
			||||||
 | 
					    async _updateHeight() {
 | 
				
			||||||
 | 
					        // wait until user has stopped scrolling
 | 
				
			||||||
 | 
					        if (this._scrollTimeout.isRunning()) {
 | 
				
			||||||
 | 
					            debuglog("updateHeight waiting for scrolling to end ... ");
 | 
				
			||||||
 | 
					            await this._scrollTimeout.finished();
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            debuglog("updateHeight getting straight to business, no scrolling going on.");
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // We might have unmounted since the timer finished, so abort if so.
 | 
				
			||||||
 | 
					        if (this.unmounted) {
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const sn = this._getScrollNode();
 | 
				
			||||||
 | 
					        const itemlist = this._itemlist.current;
 | 
				
			||||||
 | 
					        const contentHeight = this._getMessagesHeight();
 | 
				
			||||||
 | 
					        const minHeight = sn.clientHeight;
 | 
				
			||||||
 | 
					        const height = Math.max(minHeight, contentHeight);
 | 
				
			||||||
 | 
					        this._pages = Math.ceil(height / PAGE_SIZE);
 | 
				
			||||||
 | 
					        this._bottomGrowth = 0;
 | 
				
			||||||
 | 
					        const newHeight = `${this._getListHeight()}px`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const scrollState = this.scrollState;
 | 
				
			||||||
 | 
					        if (scrollState.stuckAtBottom) {
 | 
				
			||||||
 | 
					            if (itemlist.style.height !== newHeight) {
 | 
				
			||||||
 | 
					                itemlist.style.height = newHeight;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            if (sn.scrollTop !== sn.scrollHeight) {
 | 
				
			||||||
 | 
					                sn.scrollTop = sn.scrollHeight;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            debuglog("updateHeight to", newHeight);
 | 
				
			||||||
 | 
					        } else if (scrollState.trackedScrollToken) {
 | 
				
			||||||
 | 
					            const trackedNode = this._getTrackedNode();
 | 
				
			||||||
 | 
					            // if the timeline has been reloaded
 | 
				
			||||||
 | 
					            // this can be called before scrollToBottom or whatever has been called
 | 
				
			||||||
 | 
					            // so don't do anything if the node has disappeared from
 | 
				
			||||||
 | 
					            // the currently filled piece of the timeline
 | 
				
			||||||
 | 
					            if (trackedNode) {
 | 
				
			||||||
 | 
					                const oldTop = trackedNode.offsetTop;
 | 
				
			||||||
 | 
					                if (itemlist.style.height !== newHeight) {
 | 
				
			||||||
 | 
					                    itemlist.style.height = newHeight;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                const newTop = trackedNode.offsetTop;
 | 
				
			||||||
 | 
					                const topDiff = newTop - oldTop;
 | 
				
			||||||
 | 
					                // important to scroll by a relative amount as
 | 
				
			||||||
 | 
					                // reading scrollTop and then setting it might
 | 
				
			||||||
 | 
					                // yield out of date values and cause a jump
 | 
				
			||||||
 | 
					                // when setting it
 | 
				
			||||||
 | 
					                sn.scrollBy(0, topDiff);
 | 
				
			||||||
 | 
					                debuglog("updateHeight to", {newHeight, topDiff});
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    _getTrackedNode() {
 | 
				
			||||||
 | 
					        const scrollState = this.scrollState;
 | 
				
			||||||
 | 
					        const trackedNode = scrollState.trackedNode;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (!trackedNode || !trackedNode.parentElement) {
 | 
				
			||||||
 | 
					            let node;
 | 
				
			||||||
 | 
					            const messages = this._itemlist.current.children;
 | 
				
			||||||
 | 
					            const scrollToken = scrollState.trackedScrollToken;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            for (let i = messages.length-1; i >= 0; --i) {
 | 
				
			||||||
 | 
					                const m = messages[i];
 | 
				
			||||||
 | 
					                // 'data-scroll-tokens' is a DOMString of comma-separated scroll tokens
 | 
				
			||||||
 | 
					                // There might only be one scroll token
 | 
				
			||||||
 | 
					                if (m.dataset.scrollTokens &&
 | 
				
			||||||
 | 
					                    m.dataset.scrollTokens.split(',').indexOf(scrollToken) !== -1) {
 | 
				
			||||||
 | 
					                    node = m;
 | 
				
			||||||
 | 
					                    break;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            if (node) {
 | 
				
			||||||
 | 
					                debuglog("had to find tracked node again for " + scrollState.trackedScrollToken);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            scrollState.trackedNode = node;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (!scrollState.trackedNode) {
 | 
				
			||||||
 | 
					            debuglog("No node with ; '"+scrollState.trackedScrollToken+"'");
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return scrollState.trackedNode;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    _getListHeight() {
 | 
				
			||||||
 | 
					        return this._bottomGrowth + (this._pages * PAGE_SIZE);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    _getMessagesHeight() {
 | 
				
			||||||
 | 
					        const itemlist = this._itemlist.current;
 | 
				
			||||||
 | 
					        const lastNode = itemlist.lastElementChild;
 | 
				
			||||||
 | 
					        const lastNodeBottom = lastNode ? lastNode.offsetTop + lastNode.clientHeight : 0;
 | 
				
			||||||
 | 
					        const firstNodeTop = itemlist.firstElementChild ? itemlist.firstElementChild.offsetTop : 0;
 | 
				
			||||||
 | 
					        // 18 is itemlist padding
 | 
				
			||||||
 | 
					        return lastNodeBottom - firstNodeTop + (18 * 2);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    _topFromBottom(node) {
 | 
				
			||||||
 | 
					        // current capped height - distance from top = distance from bottom of container to top of tracked element
 | 
				
			||||||
 | 
					        return this._itemlist.current.clientHeight - node.offsetTop;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /* get the DOM node which has the scrollTop property we care about for our
 | 
				
			||||||
 | 
					     * message panel.
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    _getScrollNode() {
 | 
				
			||||||
 | 
					        if (this.unmounted) {
 | 
				
			||||||
 | 
					            // this shouldn't happen, but when it does, turn the NPE into
 | 
				
			||||||
 | 
					            // something more meaningful.
 | 
				
			||||||
 | 
					            throw new Error("ScrollPanel._getScrollNode called when unmounted");
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (!this._divScroll) {
 | 
				
			||||||
 | 
					            // Likewise, we should have the ref by this point, but if not
 | 
				
			||||||
 | 
					            // turn the NPE into something meaningful.
 | 
				
			||||||
 | 
					            throw new Error("ScrollPanel._getScrollNode called before AutoHideScrollbar ref collected");
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return this._divScroll;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    _collectScroll = divScroll => {
 | 
				
			||||||
 | 
					        this._divScroll = divScroll;
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					    Mark the bottom offset of the last tile so we can balance it out when
 | 
				
			||||||
 | 
					    anything below it changes, by calling updatePreventShrinking, to keep
 | 
				
			||||||
 | 
					    the same minimum bottom offset, effectively preventing the timeline to shrink.
 | 
				
			||||||
 | 
					    */
 | 
				
			||||||
 | 
					    preventShrinking = () => {
 | 
				
			||||||
 | 
					        const messageList = this._itemlist.current;
 | 
				
			||||||
 | 
					        const tiles = messageList && messageList.children;
 | 
				
			||||||
 | 
					        if (!messageList) {
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        let lastTileNode;
 | 
				
			||||||
 | 
					        for (let i = tiles.length - 1; i >= 0; i--) {
 | 
				
			||||||
 | 
					            const node = tiles[i];
 | 
				
			||||||
 | 
					            if (node.dataset.scrollTokens) {
 | 
				
			||||||
 | 
					                lastTileNode = node;
 | 
				
			||||||
 | 
					                break;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        if (!lastTileNode) {
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        this.clearPreventShrinking();
 | 
				
			||||||
 | 
					        const offsetFromBottom = messageList.clientHeight - (lastTileNode.offsetTop + lastTileNode.clientHeight);
 | 
				
			||||||
 | 
					        this.preventShrinkingState = {
 | 
				
			||||||
 | 
					            offsetFromBottom: offsetFromBottom,
 | 
				
			||||||
 | 
					            offsetNode: lastTileNode,
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					        debuglog("prevent shrinking, last tile ", offsetFromBottom, "px from bottom");
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /** Clear shrinking prevention. Used internally, and when the timeline is reloaded. */
 | 
				
			||||||
 | 
					    clearPreventShrinking = () => {
 | 
				
			||||||
 | 
					        const messageList = this._itemlist.current;
 | 
				
			||||||
 | 
					        const balanceElement = messageList && messageList.parentElement;
 | 
				
			||||||
 | 
					        if (balanceElement) balanceElement.style.paddingBottom = null;
 | 
				
			||||||
 | 
					        this.preventShrinkingState = null;
 | 
				
			||||||
 | 
					        debuglog("prevent shrinking cleared");
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					    update the container padding to balance
 | 
				
			||||||
 | 
					    the bottom offset of the last tile since
 | 
				
			||||||
 | 
					    preventShrinking was called.
 | 
				
			||||||
 | 
					    Clears the prevent-shrinking state ones the offset
 | 
				
			||||||
 | 
					    from the bottom of the marked tile grows larger than
 | 
				
			||||||
 | 
					    what it was when marking.
 | 
				
			||||||
 | 
					    */
 | 
				
			||||||
 | 
					    updatePreventShrinking = () => {
 | 
				
			||||||
 | 
					        if (this.preventShrinkingState) {
 | 
				
			||||||
 | 
					            const sn = this._getScrollNode();
 | 
				
			||||||
 | 
					            const scrollState = this.scrollState;
 | 
				
			||||||
 | 
					            const messageList = this._itemlist.current;
 | 
				
			||||||
 | 
					            const {offsetNode, offsetFromBottom} = this.preventShrinkingState;
 | 
				
			||||||
 | 
					            // element used to set paddingBottom to balance the typing notifs disappearing
 | 
				
			||||||
 | 
					            const balanceElement = messageList.parentElement;
 | 
				
			||||||
 | 
					            // if the offsetNode got unmounted, clear
 | 
				
			||||||
 | 
					            let shouldClear = !offsetNode.parentElement;
 | 
				
			||||||
 | 
					            // also if 200px from bottom
 | 
				
			||||||
 | 
					            if (!shouldClear && !scrollState.stuckAtBottom) {
 | 
				
			||||||
 | 
					                const spaceBelowViewport = sn.scrollHeight - (sn.scrollTop + sn.clientHeight);
 | 
				
			||||||
 | 
					                shouldClear = spaceBelowViewport >= 200;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            // try updating if not clearing
 | 
				
			||||||
 | 
					            if (!shouldClear) {
 | 
				
			||||||
 | 
					                const currentOffset = messageList.clientHeight - (offsetNode.offsetTop + offsetNode.clientHeight);
 | 
				
			||||||
 | 
					                const offsetDiff = offsetFromBottom - currentOffset;
 | 
				
			||||||
 | 
					                if (offsetDiff > 0) {
 | 
				
			||||||
 | 
					                    balanceElement.style.paddingBottom = `${offsetDiff}px`;
 | 
				
			||||||
 | 
					                    debuglog("update prevent shrinking ", offsetDiff, "px from bottom");
 | 
				
			||||||
 | 
					                } else if (offsetDiff < 0) {
 | 
				
			||||||
 | 
					                    shouldClear = true;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            if (shouldClear) {
 | 
				
			||||||
 | 
					                this.clearPreventShrinking();
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    render() {
 | 
				
			||||||
 | 
					        // TODO: the classnames on the div and ol could do with being updated to
 | 
				
			||||||
 | 
					        // reflect the fact that we don't necessarily contain a list of messages.
 | 
				
			||||||
 | 
					        // it's not obvious why we have a separate div and ol anyway.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // give the <ol> an explicit role=list because Safari+VoiceOver seems to think an ordered-list with
 | 
				
			||||||
 | 
					        // list-style-type: none; is no longer a list
 | 
				
			||||||
 | 
					        return (<AutoHideScrollbar wrappedRef={this._collectScroll}
 | 
				
			||||||
 | 
					                onScroll={this.onScroll}
 | 
				
			||||||
 | 
					                onWheel={this.props.onUserScroll}
 | 
				
			||||||
 | 
					                className={`mx_ScrollPanel ${this.props.className}`} style={this.props.style}>
 | 
				
			||||||
 | 
					                    { this.props.fixedChildren }
 | 
				
			||||||
 | 
					                    <div className="mx_RoomView_messageListWrapper">
 | 
				
			||||||
 | 
					                        <ol ref={this._itemlist} className="mx_RoomView_MessageList" aria-live="polite" role="list">
 | 
				
			||||||
 | 
					                            { this.props.children }
 | 
				
			||||||
 | 
					                        </ol>
 | 
				
			||||||
 | 
					                    </div>
 | 
				
			||||||
 | 
					                </AutoHideScrollbar>
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -18,7 +18,7 @@ limitations under the License.
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
import React from 'react';
 | 
					import React from 'react';
 | 
				
			||||||
import PropTypes from 'prop-types';
 | 
					import PropTypes from 'prop-types';
 | 
				
			||||||
import { _t } from '../../../languageHandler';
 | 
					import { _t, _td } from '../../../languageHandler';
 | 
				
			||||||
import * as sdk from '../../../index';
 | 
					import * as sdk from '../../../index';
 | 
				
			||||||
import Modal from "../../../Modal";
 | 
					import Modal from "../../../Modal";
 | 
				
			||||||
import PasswordReset from "../../../PasswordReset";
 | 
					import PasswordReset from "../../../PasswordReset";
 | 
				
			||||||
@ -27,7 +27,9 @@ import classNames from 'classnames';
 | 
				
			|||||||
import AuthPage from "../../views/auth/AuthPage";
 | 
					import AuthPage from "../../views/auth/AuthPage";
 | 
				
			||||||
import CountlyAnalytics from "../../../CountlyAnalytics";
 | 
					import CountlyAnalytics from "../../../CountlyAnalytics";
 | 
				
			||||||
import ServerPicker from "../../views/elements/ServerPicker";
 | 
					import ServerPicker from "../../views/elements/ServerPicker";
 | 
				
			||||||
 | 
					import PassphraseField from '../../views/auth/PassphraseField';
 | 
				
			||||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
 | 
					import {replaceableComponent} from "../../../utils/replaceableComponent";
 | 
				
			||||||
 | 
					import { PASSWORD_MIN_SCORE } from '../../views/auth/RegistrationForm';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Phases
 | 
					// Phases
 | 
				
			||||||
// Show the forgot password inputs
 | 
					// Show the forgot password inputs
 | 
				
			||||||
@ -137,10 +139,14 @@ export default class ForgotPassword extends React.Component {
 | 
				
			|||||||
        // refresh the server errors, just in case the server came back online
 | 
					        // refresh the server errors, just in case the server came back online
 | 
				
			||||||
        await this._checkServerLiveliness(this.props.serverConfig);
 | 
					        await this._checkServerLiveliness(this.props.serverConfig);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        await this['password_field'].validate({ allowEmpty: false });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (!this.state.email) {
 | 
					        if (!this.state.email) {
 | 
				
			||||||
            this.showErrorDialog(_t('The email address linked to your account must be entered.'));
 | 
					            this.showErrorDialog(_t('The email address linked to your account must be entered.'));
 | 
				
			||||||
        } else if (!this.state.password || !this.state.password2) {
 | 
					        } else if (!this.state.password || !this.state.password2) {
 | 
				
			||||||
            this.showErrorDialog(_t('A new password must be entered.'));
 | 
					            this.showErrorDialog(_t('A new password must be entered.'));
 | 
				
			||||||
 | 
					        } else if (!this.state.passwordFieldValid) {
 | 
				
			||||||
 | 
					            this.showErrorDialog(_t('Please choose a strong password'));
 | 
				
			||||||
        } else if (this.state.password !== this.state.password2) {
 | 
					        } else if (this.state.password !== this.state.password2) {
 | 
				
			||||||
            this.showErrorDialog(_t('New passwords must match each other.'));
 | 
					            this.showErrorDialog(_t('New passwords must match each other.'));
 | 
				
			||||||
        } else {
 | 
					        } else {
 | 
				
			||||||
@ -186,6 +192,12 @@ export default class ForgotPassword extends React.Component {
 | 
				
			|||||||
        });
 | 
					        });
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    onPasswordValidate(result) {
 | 
				
			||||||
 | 
					        this.setState({
 | 
				
			||||||
 | 
					            passwordFieldValid: result.valid,
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    renderForgot() {
 | 
					    renderForgot() {
 | 
				
			||||||
        const Field = sdk.getComponent('elements.Field');
 | 
					        const Field = sdk.getComponent('elements.Field');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -230,12 +242,15 @@ export default class ForgotPassword extends React.Component {
 | 
				
			|||||||
                    />
 | 
					                    />
 | 
				
			||||||
                </div>
 | 
					                </div>
 | 
				
			||||||
                <div className="mx_AuthBody_fieldRow">
 | 
					                <div className="mx_AuthBody_fieldRow">
 | 
				
			||||||
                    <Field
 | 
					                    <PassphraseField
 | 
				
			||||||
                        name="reset_password"
 | 
					                        name="reset_password"
 | 
				
			||||||
                        type="password"
 | 
					                        type="password"
 | 
				
			||||||
                        label={_t('New Password')}
 | 
					                        label={_td('New Password')}
 | 
				
			||||||
                        value={this.state.password}
 | 
					                        value={this.state.password}
 | 
				
			||||||
 | 
					                        minScore={PASSWORD_MIN_SCORE}
 | 
				
			||||||
                        onChange={this.onInputChanged.bind(this, "password")}
 | 
					                        onChange={this.onInputChanged.bind(this, "password")}
 | 
				
			||||||
 | 
					                        fieldRef={field => this['password_field'] = field}
 | 
				
			||||||
 | 
					                        onValidate={(result) => this.onPasswordValidate(result)}
 | 
				
			||||||
                        onFocus={() => CountlyAnalytics.instance.track("onboarding_forgot_password_newPassword_focus")}
 | 
					                        onFocus={() => CountlyAnalytics.instance.track("onboarding_forgot_password_newPassword_focus")}
 | 
				
			||||||
                        onBlur={() => CountlyAnalytics.instance.track("onboarding_forgot_password_newPassword_blur")}
 | 
					                        onBlur={() => CountlyAnalytics.instance.track("onboarding_forgot_password_newPassword_blur")}
 | 
				
			||||||
                        autoComplete="new-password"
 | 
					                        autoComplete="new-password"
 | 
				
			||||||
 | 
				
			|||||||
@ -40,7 +40,7 @@ enum RegistrationField {
 | 
				
			|||||||
    PasswordConfirm = "field_password_confirm",
 | 
					    PasswordConfirm = "field_password_confirm",
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const PASSWORD_MIN_SCORE = 3; // safely unguessable: moderate protection from offline slow-hash scenario.
 | 
					export const PASSWORD_MIN_SCORE = 3; // safely unguessable: moderate protection from offline slow-hash scenario.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
interface IProps {
 | 
					interface IProps {
 | 
				
			||||||
    // Values pre-filled in the input boxes when the component loads
 | 
					    // Values pre-filled in the input boxes when the component loads
 | 
				
			||||||
 | 
				
			|||||||
@ -29,7 +29,9 @@ import dis from "../../../dispatcher/dispatcher";
 | 
				
			|||||||
import IdentityAuthClient from "../../../IdentityAuthClient";
 | 
					import IdentityAuthClient from "../../../IdentityAuthClient";
 | 
				
			||||||
import Modal from "../../../Modal";
 | 
					import Modal from "../../../Modal";
 | 
				
			||||||
import {humanizeTime} from "../../../utils/humanize";
 | 
					import {humanizeTime} from "../../../utils/humanize";
 | 
				
			||||||
import createRoom, {canEncryptToAllUsers, findDMForUser, privateShouldBeEncrypted} from "../../../createRoom";
 | 
					import createRoom, {
 | 
				
			||||||
 | 
					    canEncryptToAllUsers, ensureDMExists, findDMForUser, privateShouldBeEncrypted,
 | 
				
			||||||
 | 
					} from "../../../createRoom";
 | 
				
			||||||
import {inviteMultipleToRoom, showCommunityInviteDialog} from "../../../RoomInvite";
 | 
					import {inviteMultipleToRoom, showCommunityInviteDialog} from "../../../RoomInvite";
 | 
				
			||||||
import {Key} from "../../../Keyboard";
 | 
					import {Key} from "../../../Keyboard";
 | 
				
			||||||
import {Action} from "../../../dispatcher/actions";
 | 
					import {Action} from "../../../dispatcher/actions";
 | 
				
			||||||
@ -332,6 +334,7 @@ interface IInviteDialogState {
 | 
				
			|||||||
    threepidResultsMixin: { user: Member, userId: string}[];
 | 
					    threepidResultsMixin: { user: Member, userId: string}[];
 | 
				
			||||||
    canUseIdentityServer: boolean;
 | 
					    canUseIdentityServer: boolean;
 | 
				
			||||||
    tryingIdentityServer: boolean;
 | 
					    tryingIdentityServer: boolean;
 | 
				
			||||||
 | 
					    consultFirst: boolean;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // These two flags are used for the 'Go' button to communicate what is going on.
 | 
					    // These two flags are used for the 'Go' button to communicate what is going on.
 | 
				
			||||||
    busy: boolean,
 | 
					    busy: boolean,
 | 
				
			||||||
@ -380,6 +383,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
 | 
				
			|||||||
            threepidResultsMixin: [],
 | 
					            threepidResultsMixin: [],
 | 
				
			||||||
            canUseIdentityServer: !!MatrixClientPeg.get().getIdentityServerUrl(),
 | 
					            canUseIdentityServer: !!MatrixClientPeg.get().getIdentityServerUrl(),
 | 
				
			||||||
            tryingIdentityServer: false,
 | 
					            tryingIdentityServer: false,
 | 
				
			||||||
 | 
					            consultFirst: false,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            // These two flags are used for the 'Go' button to communicate what is going on.
 | 
					            // These two flags are used for the 'Go' button to communicate what is going on.
 | 
				
			||||||
            busy: false,
 | 
					            busy: false,
 | 
				
			||||||
@ -395,6 +399,10 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private onConsultFirstChange = (ev) => {
 | 
				
			||||||
 | 
					        this.setState({consultFirst: ev.target.checked});
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    static buildRecents(excludedTargetIds: Set<string>): {userId: string, user: RoomMember, lastActive: number}[] {
 | 
					    static buildRecents(excludedTargetIds: Set<string>): {userId: string, user: RoomMember, lastActive: number}[] {
 | 
				
			||||||
        const rooms = DMRoomMap.shared().getUniqueRoomsWithIndividuals(); // map of userId => js-sdk Room
 | 
					        const rooms = DMRoomMap.shared().getUniqueRoomsWithIndividuals(); // map of userId => js-sdk Room
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -745,16 +753,34 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
 | 
				
			|||||||
            });
 | 
					            });
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        this.setState({busy: true});
 | 
					        if (this.state.consultFirst) {
 | 
				
			||||||
        try {
 | 
					            const dmRoomId = await ensureDMExists(MatrixClientPeg.get(), targetIds[0]);
 | 
				
			||||||
            await this.props.call.transfer(targetIds[0]);
 | 
					
 | 
				
			||||||
            this.setState({busy: false});
 | 
					            dis.dispatch({
 | 
				
			||||||
            this.props.onFinished();
 | 
					                action: 'place_call',
 | 
				
			||||||
        } catch (e) {
 | 
					                type: this.props.call.type,
 | 
				
			||||||
            this.setState({
 | 
					                room_id: dmRoomId,
 | 
				
			||||||
                busy: false,
 | 
					                transferee: this.props.call,
 | 
				
			||||||
                errorText: _t("Failed to transfer call"),
 | 
					 | 
				
			||||||
            });
 | 
					            });
 | 
				
			||||||
 | 
					            dis.dispatch({
 | 
				
			||||||
 | 
					                action: 'view_room',
 | 
				
			||||||
 | 
					                room_id: dmRoomId,
 | 
				
			||||||
 | 
					                should_peek: false,
 | 
				
			||||||
 | 
					                joining: false,
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					            this.props.onFinished();
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            this.setState({busy: true});
 | 
				
			||||||
 | 
					            try {
 | 
				
			||||||
 | 
					                await this.props.call.transfer(targetIds[0]);
 | 
				
			||||||
 | 
					                this.setState({busy: false});
 | 
				
			||||||
 | 
					                this.props.onFinished();
 | 
				
			||||||
 | 
					            } catch (e) {
 | 
				
			||||||
 | 
					                this.setState({
 | 
				
			||||||
 | 
					                    busy: false,
 | 
				
			||||||
 | 
					                    errorText: _t("Failed to transfer call"),
 | 
				
			||||||
 | 
					                });
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -1215,6 +1241,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
 | 
				
			|||||||
        let helpText;
 | 
					        let helpText;
 | 
				
			||||||
        let buttonText;
 | 
					        let buttonText;
 | 
				
			||||||
        let goButtonFn;
 | 
					        let goButtonFn;
 | 
				
			||||||
 | 
					        let consultSection;
 | 
				
			||||||
        let keySharingWarning = <span />;
 | 
					        let keySharingWarning = <span />;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const identityServersEnabled = SettingsStore.getValue(UIFeature.IdentityServer);
 | 
					        const identityServersEnabled = SettingsStore.getValue(UIFeature.IdentityServer);
 | 
				
			||||||
@ -1339,6 +1366,12 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
 | 
				
			|||||||
            title = _t("Transfer");
 | 
					            title = _t("Transfer");
 | 
				
			||||||
            buttonText = _t("Transfer");
 | 
					            buttonText = _t("Transfer");
 | 
				
			||||||
            goButtonFn = this._transferCall;
 | 
					            goButtonFn = this._transferCall;
 | 
				
			||||||
 | 
					            consultSection = <div>
 | 
				
			||||||
 | 
					                <label>
 | 
				
			||||||
 | 
					                    <input type="checkbox" checked={this.state.consultFirst} onChange={this.onConsultFirstChange} />
 | 
				
			||||||
 | 
					                    {_t("Consult first")}
 | 
				
			||||||
 | 
					                </label>
 | 
				
			||||||
 | 
					            </div>;
 | 
				
			||||||
        } else {
 | 
					        } else {
 | 
				
			||||||
            console.error("Unknown kind of InviteDialog: " + this.props.kind);
 | 
					            console.error("Unknown kind of InviteDialog: " + this.props.kind);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
@ -1375,6 +1408,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
 | 
				
			|||||||
                        {this._renderSection('recents')}
 | 
					                        {this._renderSection('recents')}
 | 
				
			||||||
                        {this._renderSection('suggestions')}
 | 
					                        {this._renderSection('suggestions')}
 | 
				
			||||||
                    </div>
 | 
					                    </div>
 | 
				
			||||||
 | 
					                    {consultSection}
 | 
				
			||||||
                </div>
 | 
					                </div>
 | 
				
			||||||
            </BaseDialog>
 | 
					            </BaseDialog>
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										54
									
								
								src/components/views/dialogs/SeshatResetDialog.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								src/components/views/dialogs/SeshatResetDialog.tsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,54 @@
 | 
				
			|||||||
 | 
					/*
 | 
				
			||||||
 | 
					Copyright 2021 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 React from 'react';
 | 
				
			||||||
 | 
					import {_t} from "../../../languageHandler";
 | 
				
			||||||
 | 
					import {replaceableComponent} from "../../../utils/replaceableComponent";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import BaseDialog from "./BaseDialog";
 | 
				
			||||||
 | 
					import DialogButtons from "../elements/DialogButtons";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import {IDialogProps} from "./IDialogProps";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@replaceableComponent("views.dialogs.SeshatResetDialog")
 | 
				
			||||||
 | 
					export default class SeshatResetDialog extends React.PureComponent<IDialogProps> {
 | 
				
			||||||
 | 
					    render() {
 | 
				
			||||||
 | 
					        return (
 | 
				
			||||||
 | 
					            <BaseDialog
 | 
				
			||||||
 | 
					                hasCancel={true}
 | 
				
			||||||
 | 
					                onFinished={this.props.onFinished.bind(null, false)}
 | 
				
			||||||
 | 
					                title={_t("Reset event store?")}>
 | 
				
			||||||
 | 
					                <div>
 | 
				
			||||||
 | 
					                    <p>
 | 
				
			||||||
 | 
					                        {_t("You most likely do not want to reset your event index store")}
 | 
				
			||||||
 | 
					                        <br />
 | 
				
			||||||
 | 
					                        {_t("If you do, please note that none of your messages will be deleted, " +
 | 
				
			||||||
 | 
					                            "but the search experience might be degraded for a few moments" +
 | 
				
			||||||
 | 
					                            "whilst the index is recreated",
 | 
				
			||||||
 | 
					                        )}
 | 
				
			||||||
 | 
					                    </p>
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					                <DialogButtons
 | 
				
			||||||
 | 
					                    primaryButton={_t("Reset event store")}
 | 
				
			||||||
 | 
					                    onPrimaryButtonClick={this.props.onFinished.bind(null, true)}
 | 
				
			||||||
 | 
					                    primaryButtonClass="danger"
 | 
				
			||||||
 | 
					                    cancelButton={_t("Cancel")}
 | 
				
			||||||
 | 
					                    onCancel={this.props.onFinished.bind(null, false)}
 | 
				
			||||||
 | 
					                />
 | 
				
			||||||
 | 
					            </BaseDialog>
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -19,7 +19,6 @@ import classnames from 'classnames';
 | 
				
			|||||||
import { MatrixEvent } from 'matrix-js-sdk/src/models/event';
 | 
					import { MatrixEvent } from 'matrix-js-sdk/src/models/event';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import * as Avatar from '../../../Avatar';
 | 
					import * as Avatar from '../../../Avatar';
 | 
				
			||||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
 | 
					 | 
				
			||||||
import EventTile from '../rooms/EventTile';
 | 
					import EventTile from '../rooms/EventTile';
 | 
				
			||||||
import SettingsStore from "../../../settings/SettingsStore";
 | 
					import SettingsStore from "../../../settings/SettingsStore";
 | 
				
			||||||
import {Layout} from "../../../settings/Layout";
 | 
					import {Layout} from "../../../settings/Layout";
 | 
				
			||||||
@ -41,15 +40,38 @@ interface IProps {
 | 
				
			|||||||
     * classnames to apply to the wrapper of the preview
 | 
					     * classnames to apply to the wrapper of the preview
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    className: string;
 | 
					    className: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * The ID of the displayed user
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    userId: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * The display name of the displayed user
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    displayName?: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * The mxc:// avatar URL of the displayed user
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    avatarUrl?: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Whether the EventTile should appear faded
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    faded?: boolean;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Callback for when the component is clicked
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    onClick?: () => void;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/* eslint-disable camelcase */
 | 
					 | 
				
			||||||
interface IState {
 | 
					interface IState {
 | 
				
			||||||
    userId: string;
 | 
					    message: string;
 | 
				
			||||||
    displayname: string;
 | 
					    faded: boolean;
 | 
				
			||||||
    avatar_url: string;
 | 
					    eventTileKey: number;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
/* eslint-enable camelcase */
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
const AVATAR_SIZE = 32;
 | 
					const AVATAR_SIZE = 32;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -57,45 +79,42 @@ const AVATAR_SIZE = 32;
 | 
				
			|||||||
export default class EventTilePreview extends React.Component<IProps, IState> {
 | 
					export default class EventTilePreview extends React.Component<IProps, IState> {
 | 
				
			||||||
    constructor(props: IProps) {
 | 
					    constructor(props: IProps) {
 | 
				
			||||||
        super(props);
 | 
					        super(props);
 | 
				
			||||||
 | 
					 | 
				
			||||||
        this.state = {
 | 
					        this.state = {
 | 
				
			||||||
            userId: "@erim:fink.fink",
 | 
					            message: props.message,
 | 
				
			||||||
            displayname: "Erimayas Fink",
 | 
					            faded: !!props.faded,
 | 
				
			||||||
            avatar_url: null,
 | 
					            eventTileKey: 0,
 | 
				
			||||||
        };
 | 
					        };
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async componentDidMount() {
 | 
					    changeMessage(message: string) {
 | 
				
			||||||
        // Fetch current user data
 | 
					 | 
				
			||||||
        const client = MatrixClientPeg.get();
 | 
					 | 
				
			||||||
        const userId = client.getUserId();
 | 
					 | 
				
			||||||
        const profileInfo = await client.getProfileInfo(userId);
 | 
					 | 
				
			||||||
        const avatarUrl = profileInfo.avatar_url;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        this.setState({
 | 
					        this.setState({
 | 
				
			||||||
            userId,
 | 
					            message,
 | 
				
			||||||
            displayname: profileInfo.displayname,
 | 
					            // Change the EventTile key to force React to create a new instance
 | 
				
			||||||
            avatar_url: avatarUrl,
 | 
					            eventTileKey: this.state.eventTileKey + 1,
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private fakeEvent({userId, displayname, avatar_url: avatarUrl}: IState) {
 | 
					    unfade() {
 | 
				
			||||||
 | 
					        this.setState({ faded: false });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private fakeEvent({message}: IState) {
 | 
				
			||||||
        // Fake it till we make it
 | 
					        // Fake it till we make it
 | 
				
			||||||
        /* eslint-disable quote-props */
 | 
					        /* eslint-disable quote-props */
 | 
				
			||||||
        const rawEvent = {
 | 
					        const rawEvent = {
 | 
				
			||||||
            type: "m.room.message",
 | 
					            type: "m.room.message",
 | 
				
			||||||
            sender: userId,
 | 
					            sender: this.props.userId,
 | 
				
			||||||
            content: {
 | 
					            content: {
 | 
				
			||||||
                "m.new_content": {
 | 
					                "m.new_content": {
 | 
				
			||||||
                    msgtype: "m.text",
 | 
					                    msgtype: "m.text",
 | 
				
			||||||
                    body: this.props.message,
 | 
					                    body: message,
 | 
				
			||||||
                    displayname: displayname,
 | 
					                    displayname: this.props.displayName,
 | 
				
			||||||
                    avatar_url: avatarUrl,
 | 
					                    avatar_url: this.props.avatarUrl,
 | 
				
			||||||
                },
 | 
					                },
 | 
				
			||||||
                msgtype: "m.text",
 | 
					                msgtype: "m.text",
 | 
				
			||||||
                body: this.props.message,
 | 
					                body: message,
 | 
				
			||||||
                displayname: displayname,
 | 
					                displayname: this.props.displayName,
 | 
				
			||||||
                avatar_url: avatarUrl,
 | 
					                avatar_url: this.props.avatarUrl,
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
            unsigned: {
 | 
					            unsigned: {
 | 
				
			||||||
                age: 97,
 | 
					                age: 97,
 | 
				
			||||||
@ -108,12 +127,15 @@ export default class EventTilePreview extends React.Component<IProps, IState> {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        // Fake it more
 | 
					        // Fake it more
 | 
				
			||||||
        event.sender = {
 | 
					        event.sender = {
 | 
				
			||||||
            name: displayname,
 | 
					            name: this.props.displayName,
 | 
				
			||||||
            userId: userId,
 | 
					            userId: this.props.userId,
 | 
				
			||||||
            getAvatarUrl: (..._) => {
 | 
					            getAvatarUrl: (..._) => {
 | 
				
			||||||
                return Avatar.avatarUrlForUser({avatarUrl}, AVATAR_SIZE, AVATAR_SIZE, "crop");
 | 
					                return Avatar.avatarUrlForUser(
 | 
				
			||||||
 | 
					                    { avatarUrl: this.props.avatarUrl },
 | 
				
			||||||
 | 
					                    AVATAR_SIZE, AVATAR_SIZE, "crop",
 | 
				
			||||||
 | 
					                );
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
            getMxcAvatarUrl: () => avatarUrl,
 | 
					            getMxcAvatarUrl: () => this.props.avatarUrl,
 | 
				
			||||||
        };
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return event;
 | 
					        return event;
 | 
				
			||||||
@ -125,10 +147,12 @@ export default class EventTilePreview extends React.Component<IProps, IState> {
 | 
				
			|||||||
        const className = classnames(this.props.className, {
 | 
					        const className = classnames(this.props.className, {
 | 
				
			||||||
            "mx_IRCLayout": this.props.layout == Layout.IRC,
 | 
					            "mx_IRCLayout": this.props.layout == Layout.IRC,
 | 
				
			||||||
            "mx_GroupLayout": this.props.layout == Layout.Group,
 | 
					            "mx_GroupLayout": this.props.layout == Layout.Group,
 | 
				
			||||||
 | 
					            "mx_EventTilePreview_faded": this.state.faded,
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return <div className={className}>
 | 
					        return <div className={className} onClick={this.props.onClick}>
 | 
				
			||||||
            <EventTile
 | 
					            <EventTile
 | 
				
			||||||
 | 
					                key={this.state.eventTileKey}
 | 
				
			||||||
                mxEvent={event}
 | 
					                mxEvent={event}
 | 
				
			||||||
                layout={this.props.layout}
 | 
					                layout={this.props.layout}
 | 
				
			||||||
                enableFlair={SettingsStore.getValue(UIFeature.Flair)}
 | 
					                enableFlair={SettingsStore.getValue(UIFeature.Flair)}
 | 
				
			||||||
 | 
				
			|||||||
@ -485,16 +485,14 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
 | 
				
			|||||||
        if (model.autoComplete && model.autoComplete.hasCompletions()) {
 | 
					        if (model.autoComplete && model.autoComplete.hasCompletions()) {
 | 
				
			||||||
            const autoComplete = model.autoComplete;
 | 
					            const autoComplete = model.autoComplete;
 | 
				
			||||||
            switch (autocompleteAction) {
 | 
					            switch (autocompleteAction) {
 | 
				
			||||||
 | 
					                case AutocompleteAction.CompleteOrPrevSelection:
 | 
				
			||||||
                case AutocompleteAction.PrevSelection:
 | 
					                case AutocompleteAction.PrevSelection:
 | 
				
			||||||
                    autoComplete.onUpArrow(event);
 | 
					                    autoComplete.selectPreviousSelection();
 | 
				
			||||||
                    handled = true;
 | 
					                    handled = true;
 | 
				
			||||||
                    break;
 | 
					                    break;
 | 
				
			||||||
 | 
					                case AutocompleteAction.CompleteOrNextSelection:
 | 
				
			||||||
                case AutocompleteAction.NextSelection:
 | 
					                case AutocompleteAction.NextSelection:
 | 
				
			||||||
                    autoComplete.onDownArrow(event);
 | 
					                    autoComplete.selectNextSelection();
 | 
				
			||||||
                    handled = true;
 | 
					 | 
				
			||||||
                    break;
 | 
					 | 
				
			||||||
                case AutocompleteAction.ApplySelection:
 | 
					 | 
				
			||||||
                    autoComplete.onTab(event);
 | 
					 | 
				
			||||||
                    handled = true;
 | 
					                    handled = true;
 | 
				
			||||||
                    break;
 | 
					                    break;
 | 
				
			||||||
                case AutocompleteAction.Cancel:
 | 
					                case AutocompleteAction.Cancel:
 | 
				
			||||||
@ -504,8 +502,10 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
 | 
				
			|||||||
                default:
 | 
					                default:
 | 
				
			||||||
                    return; // don't preventDefault on anything else
 | 
					                    return; // don't preventDefault on anything else
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        } else if (autocompleteAction === AutocompleteAction.ApplySelection) {
 | 
					        } else if (autocompleteAction === AutocompleteAction.CompleteOrPrevSelection
 | 
				
			||||||
            this.tabCompleteName(event);
 | 
					            || autocompleteAction === AutocompleteAction.CompleteOrNextSelection) {
 | 
				
			||||||
 | 
					            // there is no current autocomplete window, try to open it
 | 
				
			||||||
 | 
					            this.tabCompleteName();
 | 
				
			||||||
            handled = true;
 | 
					            handled = true;
 | 
				
			||||||
        } else if (event.key === Key.BACKSPACE || event.key === Key.DELETE) {
 | 
					        } else if (event.key === Key.BACKSPACE || event.key === Key.DELETE) {
 | 
				
			||||||
            this.formatBarRef.current.hide();
 | 
					            this.formatBarRef.current.hide();
 | 
				
			||||||
@ -517,7 +517,7 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private async tabCompleteName(event: React.KeyboardEvent) {
 | 
					    private async tabCompleteName() {
 | 
				
			||||||
        try {
 | 
					        try {
 | 
				
			||||||
            await new Promise<void>(resolve => this.setState({showVisualBell: false}, resolve));
 | 
					            await new Promise<void>(resolve => this.setState({showVisualBell: false}, resolve));
 | 
				
			||||||
            const {model} = this.props;
 | 
					            const {model} = this.props;
 | 
				
			||||||
@ -540,7 +540,7 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
            // Don't try to do things with the autocomplete if there is none shown
 | 
					            // Don't try to do things with the autocomplete if there is none shown
 | 
				
			||||||
            if (model.autoComplete) {
 | 
					            if (model.autoComplete) {
 | 
				
			||||||
                await model.autoComplete.onTab(event);
 | 
					                await model.autoComplete.startSelection();
 | 
				
			||||||
                if (!model.autoComplete.hasSelection()) {
 | 
					                if (!model.autoComplete.hasSelection()) {
 | 
				
			||||||
                    this.setState({showVisualBell: true});
 | 
					                    this.setState({showVisualBell: true});
 | 
				
			||||||
                    model.autoComplete.close();
 | 
					                    model.autoComplete.close();
 | 
				
			||||||
 | 
				
			|||||||
@ -936,7 +936,7 @@ export default class EventTile extends React.Component {
 | 
				
			|||||||
            );
 | 
					            );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const TooltipButton = sdk.getComponent('elements.TooltipButton');
 | 
					        const TooltipButton = sdk.getComponent('elements.TooltipButton');
 | 
				
			||||||
        const keyRequestInfo = isEncryptionFailure ?
 | 
					        const keyRequestInfo = isEncryptionFailure && !isRedacted ?
 | 
				
			||||||
            <div className="mx_EventTile_keyRequestInfo">
 | 
					            <div className="mx_EventTile_keyRequestInfo">
 | 
				
			||||||
                <span className="mx_EventTile_keyRequestInfo_text">
 | 
					                <span className="mx_EventTile_keyRequestInfo_text">
 | 
				
			||||||
                    { keyRequestInfoContent }
 | 
					                    { keyRequestInfoContent }
 | 
				
			||||||
 | 
				
			|||||||
@ -25,6 +25,7 @@ import classNames from 'classnames';
 | 
				
			|||||||
import { _t } from '../../../languageHandler';
 | 
					import { _t } from '../../../languageHandler';
 | 
				
			||||||
import SdkConfig from "../../../SdkConfig";
 | 
					import SdkConfig from "../../../SdkConfig";
 | 
				
			||||||
import IdentityAuthClient from '../../../IdentityAuthClient';
 | 
					import IdentityAuthClient from '../../../IdentityAuthClient';
 | 
				
			||||||
 | 
					import SettingsStore from "../../../settings/SettingsStore";
 | 
				
			||||||
import {CommunityPrototypeStore} from "../../../stores/CommunityPrototypeStore";
 | 
					import {CommunityPrototypeStore} from "../../../stores/CommunityPrototypeStore";
 | 
				
			||||||
import {UPDATE_EVENT} from "../../../stores/AsyncStore";
 | 
					import {UPDATE_EVENT} from "../../../stores/AsyncStore";
 | 
				
			||||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
 | 
					import {replaceableComponent} from "../../../utils/replaceableComponent";
 | 
				
			||||||
@ -302,10 +303,12 @@ export default class RoomPreviewBar extends React.Component {
 | 
				
			|||||||
        const brand = SdkConfig.get().brand;
 | 
					        const brand = SdkConfig.get().brand;
 | 
				
			||||||
        const Spinner = sdk.getComponent('elements.Spinner');
 | 
					        const Spinner = sdk.getComponent('elements.Spinner');
 | 
				
			||||||
        const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
 | 
					        const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
 | 
				
			||||||
 | 
					        const EventTilePreview = sdk.getComponent('elements.EventTilePreview');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        let showSpinner = false;
 | 
					        let showSpinner = false;
 | 
				
			||||||
        let title;
 | 
					        let title;
 | 
				
			||||||
        let subTitle;
 | 
					        let subTitle;
 | 
				
			||||||
 | 
					        let reasonElement;
 | 
				
			||||||
        let primaryActionHandler;
 | 
					        let primaryActionHandler;
 | 
				
			||||||
        let primaryActionLabel;
 | 
					        let primaryActionLabel;
 | 
				
			||||||
        let secondaryActionHandler;
 | 
					        let secondaryActionHandler;
 | 
				
			||||||
@ -491,6 +494,29 @@ export default class RoomPreviewBar extends React.Component {
 | 
				
			|||||||
                    primaryActionLabel = _t("Accept");
 | 
					                    primaryActionLabel = _t("Accept");
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                const myUserId = MatrixClientPeg.get().getUserId();
 | 
				
			||||||
 | 
					                const reason = this.props.room.currentState.getMember(myUserId).events.member.event.content.reason;
 | 
				
			||||||
 | 
					                if (reason) {
 | 
				
			||||||
 | 
					                    this.reasonElement = React.createRef();
 | 
				
			||||||
 | 
					                    // We hide the reason for invitation by default, since it can be a
 | 
				
			||||||
 | 
					                    // vector for spam/harassment.
 | 
				
			||||||
 | 
					                    const showReason = () => {
 | 
				
			||||||
 | 
					                        this.reasonElement.current.unfade();
 | 
				
			||||||
 | 
					                        this.reasonElement.current.changeMessage(reason);
 | 
				
			||||||
 | 
					                    };
 | 
				
			||||||
 | 
					                    reasonElement = <EventTilePreview
 | 
				
			||||||
 | 
					                        ref={this.reasonElement}
 | 
				
			||||||
 | 
					                        onClick={showReason}
 | 
				
			||||||
 | 
					                        className="mx_RoomPreviewBar_reason"
 | 
				
			||||||
 | 
					                        message={_t("Invite messages are hidden by default. Click to show the message.")}
 | 
				
			||||||
 | 
					                        layout={SettingsStore.getValue("layout")}
 | 
				
			||||||
 | 
					                        userId={inviteMember.userId}
 | 
				
			||||||
 | 
					                        displayName={inviteMember.rawDisplayName}
 | 
				
			||||||
 | 
					                        avatarUrl={inviteMember.events.member.event.content.avatar_url}
 | 
				
			||||||
 | 
					                        faded={true}
 | 
				
			||||||
 | 
					                    />;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                primaryActionHandler = this.props.onJoinClick;
 | 
					                primaryActionHandler = this.props.onJoinClick;
 | 
				
			||||||
                secondaryActionLabel = _t("Reject");
 | 
					                secondaryActionLabel = _t("Reject");
 | 
				
			||||||
                secondaryActionHandler = this.props.onRejectClick;
 | 
					                secondaryActionHandler = this.props.onRejectClick;
 | 
				
			||||||
@ -582,6 +608,7 @@ export default class RoomPreviewBar extends React.Component {
 | 
				
			|||||||
                    { titleElement }
 | 
					                    { titleElement }
 | 
				
			||||||
                    { subTitleElements }
 | 
					                    { subTitleElements }
 | 
				
			||||||
                </div>
 | 
					                </div>
 | 
				
			||||||
 | 
					                { reasonElement }
 | 
				
			||||||
                <div className="mx_RoomPreviewBar_actions">
 | 
					                <div className="mx_RoomPreviewBar_actions">
 | 
				
			||||||
                    { secondaryButton }
 | 
					                    { secondaryButton }
 | 
				
			||||||
                    { extraComponents }
 | 
					                    { extraComponents }
 | 
				
			||||||
 | 
				
			|||||||
@ -28,13 +28,12 @@ import Modal from "../../../Modal";
 | 
				
			|||||||
import PassphraseField from "../auth/PassphraseField";
 | 
					import PassphraseField from "../auth/PassphraseField";
 | 
				
			||||||
import CountlyAnalytics from "../../../CountlyAnalytics";
 | 
					import CountlyAnalytics from "../../../CountlyAnalytics";
 | 
				
			||||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
 | 
					import {replaceableComponent} from "../../../utils/replaceableComponent";
 | 
				
			||||||
 | 
					import { PASSWORD_MIN_SCORE } from '../auth/RegistrationForm';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const FIELD_OLD_PASSWORD = 'field_old_password';
 | 
					const FIELD_OLD_PASSWORD = 'field_old_password';
 | 
				
			||||||
const FIELD_NEW_PASSWORD = 'field_new_password';
 | 
					const FIELD_NEW_PASSWORD = 'field_new_password';
 | 
				
			||||||
const FIELD_NEW_PASSWORD_CONFIRM = 'field_new_password_confirm';
 | 
					const FIELD_NEW_PASSWORD_CONFIRM = 'field_new_password_confirm';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const PASSWORD_MIN_SCORE = 3; // safely unguessable: moderate protection from offline slow-hash scenario.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
@replaceableComponent("views.settings.ChangePassword")
 | 
					@replaceableComponent("views.settings.ChangePassword")
 | 
				
			||||||
export default class ChangePassword extends React.Component {
 | 
					export default class ChangePassword extends React.Component {
 | 
				
			||||||
    static propTypes = {
 | 
					    static propTypes = {
 | 
				
			||||||
 | 
				
			|||||||
@ -26,6 +26,7 @@ import {formatBytes, formatCountLong} from "../../../utils/FormattingUtils";
 | 
				
			|||||||
import EventIndexPeg from "../../../indexing/EventIndexPeg";
 | 
					import EventIndexPeg from "../../../indexing/EventIndexPeg";
 | 
				
			||||||
import {SettingLevel} from "../../../settings/SettingLevel";
 | 
					import {SettingLevel} from "../../../settings/SettingLevel";
 | 
				
			||||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
 | 
					import {replaceableComponent} from "../../../utils/replaceableComponent";
 | 
				
			||||||
 | 
					import SeshatResetDialog from '../dialogs/SeshatResetDialog';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@replaceableComponent("views.settings.EventIndexPanel")
 | 
					@replaceableComponent("views.settings.EventIndexPanel")
 | 
				
			||||||
export default class EventIndexPanel extends React.Component {
 | 
					export default class EventIndexPanel extends React.Component {
 | 
				
			||||||
@ -122,6 +123,20 @@ export default class EventIndexPanel extends React.Component {
 | 
				
			|||||||
        await this.updateState();
 | 
					        await this.updateState();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    _confirmEventStoreReset = () => {
 | 
				
			||||||
 | 
					        const self = this;
 | 
				
			||||||
 | 
					        const { close } = Modal.createDialog(SeshatResetDialog, {
 | 
				
			||||||
 | 
					            onFinished: async (success) => {
 | 
				
			||||||
 | 
					                if (success) {
 | 
				
			||||||
 | 
					                    await SettingsStore.setValue('enableEventIndexing', null, SettingLevel.DEVICE, false);
 | 
				
			||||||
 | 
					                    await EventIndexPeg.deleteEventIndex();
 | 
				
			||||||
 | 
					                    await self._onEnable();
 | 
				
			||||||
 | 
					                    close();
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    render() {
 | 
					    render() {
 | 
				
			||||||
        let eventIndexingSettings = null;
 | 
					        let eventIndexingSettings = null;
 | 
				
			||||||
        const InlineSpinner = sdk.getComponent('elements.InlineSpinner');
 | 
					        const InlineSpinner = sdk.getComponent('elements.InlineSpinner');
 | 
				
			||||||
@ -167,7 +182,7 @@ export default class EventIndexPanel extends React.Component {
 | 
				
			|||||||
            );
 | 
					            );
 | 
				
			||||||
        } else if (EventIndexPeg.platformHasSupport() && !EventIndexPeg.supportIsInstalled()) {
 | 
					        } else if (EventIndexPeg.platformHasSupport() && !EventIndexPeg.supportIsInstalled()) {
 | 
				
			||||||
            const nativeLink = (
 | 
					            const nativeLink = (
 | 
				
			||||||
                "https://github.com/vector-im/element-web/blob/develop/" +
 | 
					                "https://github.com/vector-im/element-desktop/blob/develop/" +
 | 
				
			||||||
                "docs/native-node-modules.md#" +
 | 
					                "docs/native-node-modules.md#" +
 | 
				
			||||||
                "adding-seshat-for-search-in-e2e-encrypted-rooms"
 | 
					                "adding-seshat-for-search-in-e2e-encrypted-rooms"
 | 
				
			||||||
            );
 | 
					            );
 | 
				
			||||||
@ -212,7 +227,10 @@ export default class EventIndexPanel extends React.Component {
 | 
				
			|||||||
            eventIndexingSettings = (
 | 
					            eventIndexingSettings = (
 | 
				
			||||||
                <div className='mx_SettingsTab_subsectionText'>
 | 
					                <div className='mx_SettingsTab_subsectionText'>
 | 
				
			||||||
                    <p>
 | 
					                    <p>
 | 
				
			||||||
                        {_t("Message search initialisation failed")}
 | 
					                        {this.state.enabling
 | 
				
			||||||
 | 
					                            ? <InlineSpinner />
 | 
				
			||||||
 | 
					                            : _t("Message search initilisation failed")
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
                    </p>
 | 
					                    </p>
 | 
				
			||||||
                    {EventIndexPeg.error && (
 | 
					                    {EventIndexPeg.error && (
 | 
				
			||||||
                    <details>
 | 
					                    <details>
 | 
				
			||||||
@ -220,6 +238,11 @@ export default class EventIndexPanel extends React.Component {
 | 
				
			|||||||
                        <code>
 | 
					                        <code>
 | 
				
			||||||
                            {EventIndexPeg.error.message}
 | 
					                            {EventIndexPeg.error.message}
 | 
				
			||||||
                        </code>
 | 
					                        </code>
 | 
				
			||||||
 | 
					                        <p>
 | 
				
			||||||
 | 
					                            <AccessibleButton key="delete" kind="danger" onClick={this._confirmEventStoreReset}>
 | 
				
			||||||
 | 
					                                {_t("Reset")}
 | 
				
			||||||
 | 
					                            </AccessibleButton>
 | 
				
			||||||
 | 
					                        </p>
 | 
				
			||||||
                    </details>
 | 
					                    </details>
 | 
				
			||||||
                    )}
 | 
					                    )}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -18,6 +18,7 @@ limitations under the License.
 | 
				
			|||||||
import React from 'react';
 | 
					import React from 'react';
 | 
				
			||||||
import {_t} from "../../../../../languageHandler";
 | 
					import {_t} from "../../../../../languageHandler";
 | 
				
			||||||
import SdkConfig from "../../../../../SdkConfig";
 | 
					import SdkConfig from "../../../../../SdkConfig";
 | 
				
			||||||
 | 
					import { MatrixClientPeg } from '../../../../../MatrixClientPeg';
 | 
				
			||||||
import SettingsStore from "../../../../../settings/SettingsStore";
 | 
					import SettingsStore from "../../../../../settings/SettingsStore";
 | 
				
			||||||
import { enumerateThemes } from "../../../../../theme";
 | 
					import { enumerateThemes } from "../../../../../theme";
 | 
				
			||||||
import ThemeWatcher from "../../../../../settings/watchers/ThemeWatcher";
 | 
					import ThemeWatcher from "../../../../../settings/watchers/ThemeWatcher";
 | 
				
			||||||
@ -63,6 +64,10 @@ interface IState extends IThemeState {
 | 
				
			|||||||
    systemFont: string;
 | 
					    systemFont: string;
 | 
				
			||||||
    showAdvanced: boolean;
 | 
					    showAdvanced: boolean;
 | 
				
			||||||
    layout: Layout;
 | 
					    layout: Layout;
 | 
				
			||||||
 | 
					    // User profile data for the message preview
 | 
				
			||||||
 | 
					    userId: string;
 | 
				
			||||||
 | 
					    displayName: string;
 | 
				
			||||||
 | 
					    avatarUrl: string;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@replaceableComponent("views.settings.tabs.user.AppearanceUserSettingsTab")
 | 
					@replaceableComponent("views.settings.tabs.user.AppearanceUserSettingsTab")
 | 
				
			||||||
@ -84,9 +89,25 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
 | 
				
			|||||||
            systemFont: SettingsStore.getValue("systemFont"),
 | 
					            systemFont: SettingsStore.getValue("systemFont"),
 | 
				
			||||||
            showAdvanced: false,
 | 
					            showAdvanced: false,
 | 
				
			||||||
            layout: SettingsStore.getValue("layout"),
 | 
					            layout: SettingsStore.getValue("layout"),
 | 
				
			||||||
 | 
					            userId: "@erim:fink.fink",
 | 
				
			||||||
 | 
					            displayName: "Erimayas Fink",
 | 
				
			||||||
 | 
					            avatarUrl: null,
 | 
				
			||||||
        };
 | 
					        };
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async componentDidMount() {
 | 
				
			||||||
 | 
					        // Fetch the current user profile for the message preview
 | 
				
			||||||
 | 
					        const client = MatrixClientPeg.get();
 | 
				
			||||||
 | 
					        const userId = client.getUserId();
 | 
				
			||||||
 | 
					        const profileInfo = await client.getProfileInfo(userId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        this.setState({
 | 
				
			||||||
 | 
					            userId,
 | 
				
			||||||
 | 
					            displayName: profileInfo.displayname,
 | 
				
			||||||
 | 
					            avatarUrl: profileInfo.avatar_url,
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private calculateThemeState(): IThemeState {
 | 
					    private calculateThemeState(): IThemeState {
 | 
				
			||||||
        // We have to mirror the logic from ThemeWatcher.getEffectiveTheme so we
 | 
					        // We have to mirror the logic from ThemeWatcher.getEffectiveTheme so we
 | 
				
			||||||
        // show the right values for things.
 | 
					        // show the right values for things.
 | 
				
			||||||
@ -307,6 +328,9 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
 | 
				
			|||||||
                className="mx_AppearanceUserSettingsTab_fontSlider_preview"
 | 
					                className="mx_AppearanceUserSettingsTab_fontSlider_preview"
 | 
				
			||||||
                message={this.MESSAGE_PREVIEW_TEXT}
 | 
					                message={this.MESSAGE_PREVIEW_TEXT}
 | 
				
			||||||
                layout={this.state.layout}
 | 
					                layout={this.state.layout}
 | 
				
			||||||
 | 
					                userId={this.state.userId}
 | 
				
			||||||
 | 
					                displayName={this.state.displayName}
 | 
				
			||||||
 | 
					                avatarUrl={this.state.avatarUrl}
 | 
				
			||||||
            />
 | 
					            />
 | 
				
			||||||
            <div className="mx_AppearanceUserSettingsTab_fontSlider">
 | 
					            <div className="mx_AppearanceUserSettingsTab_fontSlider">
 | 
				
			||||||
                <div className="mx_AppearanceUserSettingsTab_fontSlider_smallText">Aa</div>
 | 
					                <div className="mx_AppearanceUserSettingsTab_fontSlider_smallText">Aa</div>
 | 
				
			||||||
 | 
				
			|||||||
@ -364,6 +364,11 @@ export default class CallView extends React.Component<IProps, IState> {
 | 
				
			|||||||
        CallHandler.sharedInstance().setActiveCallRoomId(userFacingRoomId);
 | 
					        CallHandler.sharedInstance().setActiveCallRoomId(userFacingRoomId);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private onTransferClick = () => {
 | 
				
			||||||
 | 
					        const transfereeCall = CallHandler.sharedInstance().getTransfereeForCallId(this.props.call.callId);
 | 
				
			||||||
 | 
					        this.props.call.transferToCall(transfereeCall);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public render() {
 | 
					    public render() {
 | 
				
			||||||
        const client = MatrixClientPeg.get();
 | 
					        const client = MatrixClientPeg.get();
 | 
				
			||||||
        const callRoomId = CallHandler.roomIdForCall(this.props.call);
 | 
					        const callRoomId = CallHandler.roomIdForCall(this.props.call);
 | 
				
			||||||
@ -479,25 +484,52 @@ export default class CallView extends React.Component<IProps, IState> {
 | 
				
			|||||||
        // for voice calls (fills the bg)
 | 
					        // for voice calls (fills the bg)
 | 
				
			||||||
        let contentView: React.ReactNode;
 | 
					        let contentView: React.ReactNode;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const transfereeCall = CallHandler.sharedInstance().getTransfereeForCallId(this.props.call.callId);
 | 
				
			||||||
        const isOnHold = this.state.isLocalOnHold || this.state.isRemoteOnHold;
 | 
					        const isOnHold = this.state.isLocalOnHold || this.state.isRemoteOnHold;
 | 
				
			||||||
        let onHoldText = null;
 | 
					        let holdTransferContent;
 | 
				
			||||||
        if (this.state.isRemoteOnHold) {
 | 
					        if (transfereeCall) {
 | 
				
			||||||
            const holdString = CallHandler.sharedInstance().hasAnyUnheldCall() ?
 | 
					            const transferTargetRoom = MatrixClientPeg.get().getRoom(CallHandler.roomIdForCall(this.props.call));
 | 
				
			||||||
                _td("You held the call <a>Switch</a>") : _td("You held the call <a>Resume</a>");
 | 
					            const transferTargetName = transferTargetRoom ? transferTargetRoom.name : _t("unknown person");
 | 
				
			||||||
            onHoldText = _t(holdString, {}, {
 | 
					
 | 
				
			||||||
                a: sub => <AccessibleButton kind="link" onClick={this.onCallResumeClick}>
 | 
					            const transfereeRoom = MatrixClientPeg.get().getRoom(
 | 
				
			||||||
                    {sub}
 | 
					                CallHandler.roomIdForCall(transfereeCall),
 | 
				
			||||||
                </AccessibleButton>,
 | 
					            );
 | 
				
			||||||
            });
 | 
					            const transfereeName = transfereeRoom ? transfereeRoom.name : _t("unknown person");
 | 
				
			||||||
        } else if (this.state.isLocalOnHold) {
 | 
					
 | 
				
			||||||
            onHoldText = _t("%(peerName)s held the call", {
 | 
					            holdTransferContent = <div className="mx_CallView_holdTransferContent">
 | 
				
			||||||
                peerName: this.props.call.getOpponentMember().name,
 | 
					                {_t(
 | 
				
			||||||
            });
 | 
					                    "Consulting with %(transferTarget)s. <a>Transfer to %(transferee)s</a>",
 | 
				
			||||||
 | 
					                    {
 | 
				
			||||||
 | 
					                        transferTarget: transferTargetName,
 | 
				
			||||||
 | 
					                        transferee: transfereeName,
 | 
				
			||||||
 | 
					                    },
 | 
				
			||||||
 | 
					                    {
 | 
				
			||||||
 | 
					                        a: sub => <AccessibleButton kind="link" onClick={this.onTransferClick}>{sub}</AccessibleButton>,
 | 
				
			||||||
 | 
					                    },
 | 
				
			||||||
 | 
					                )}
 | 
				
			||||||
 | 
					            </div>;
 | 
				
			||||||
 | 
					        } else if (isOnHold) {
 | 
				
			||||||
 | 
					            let onHoldText = null;
 | 
				
			||||||
 | 
					            if (this.state.isRemoteOnHold) {
 | 
				
			||||||
 | 
					                const holdString = CallHandler.sharedInstance().hasAnyUnheldCall() ?
 | 
				
			||||||
 | 
					                    _td("You held the call <a>Switch</a>") : _td("You held the call <a>Resume</a>");
 | 
				
			||||||
 | 
					                onHoldText = _t(holdString, {}, {
 | 
				
			||||||
 | 
					                    a: sub => <AccessibleButton kind="link" onClick={this.onCallResumeClick}>
 | 
				
			||||||
 | 
					                        {sub}
 | 
				
			||||||
 | 
					                    </AccessibleButton>,
 | 
				
			||||||
 | 
					                });
 | 
				
			||||||
 | 
					            } else if (this.state.isLocalOnHold) {
 | 
				
			||||||
 | 
					                onHoldText = _t("%(peerName)s held the call", {
 | 
				
			||||||
 | 
					                    peerName: this.props.call.getOpponentMember().name,
 | 
				
			||||||
 | 
					                });
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            holdTransferContent = <div className="mx_CallView_holdTransferContent">
 | 
				
			||||||
 | 
					                {onHoldText}
 | 
				
			||||||
 | 
					            </div>;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (this.props.call.type === CallType.Video) {
 | 
					        if (this.props.call.type === CallType.Video) {
 | 
				
			||||||
            let localVideoFeed = null;
 | 
					            let localVideoFeed = null;
 | 
				
			||||||
            let onHoldContent = null;
 | 
					 | 
				
			||||||
            let onHoldBackground = null;
 | 
					            let onHoldBackground = null;
 | 
				
			||||||
            const backgroundStyle: CSSProperties = {};
 | 
					            const backgroundStyle: CSSProperties = {};
 | 
				
			||||||
            const containerClasses = classNames({
 | 
					            const containerClasses = classNames({
 | 
				
			||||||
@ -505,9 +537,6 @@ export default class CallView extends React.Component<IProps, IState> {
 | 
				
			|||||||
                mx_CallView_video_hold: isOnHold,
 | 
					                mx_CallView_video_hold: isOnHold,
 | 
				
			||||||
            });
 | 
					            });
 | 
				
			||||||
            if (isOnHold) {
 | 
					            if (isOnHold) {
 | 
				
			||||||
                onHoldContent = <div className="mx_CallView_video_holdContent">
 | 
					 | 
				
			||||||
                    {onHoldText}
 | 
					 | 
				
			||||||
                </div>;
 | 
					 | 
				
			||||||
                const backgroundAvatarUrl = avatarUrlForMember(
 | 
					                const backgroundAvatarUrl = avatarUrlForMember(
 | 
				
			||||||
                    // is it worth getting the size of the div to pass here?
 | 
					                    // is it worth getting the size of the div to pass here?
 | 
				
			||||||
                    this.props.call.getOpponentMember(), 1024, 1024, 'crop',
 | 
					                    this.props.call.getOpponentMember(), 1024, 1024, 'crop',
 | 
				
			||||||
@ -534,7 +563,7 @@ export default class CallView extends React.Component<IProps, IState> {
 | 
				
			|||||||
                    maxHeight={maxVideoHeight}
 | 
					                    maxHeight={maxVideoHeight}
 | 
				
			||||||
                />
 | 
					                />
 | 
				
			||||||
                {localVideoFeed}
 | 
					                {localVideoFeed}
 | 
				
			||||||
                {onHoldContent}
 | 
					                {holdTransferContent}
 | 
				
			||||||
                {callControls}
 | 
					                {callControls}
 | 
				
			||||||
            </div>;
 | 
					            </div>;
 | 
				
			||||||
        } else {
 | 
					        } else {
 | 
				
			||||||
@ -554,7 +583,7 @@ export default class CallView extends React.Component<IProps, IState> {
 | 
				
			|||||||
                        />
 | 
					                        />
 | 
				
			||||||
                    </div>
 | 
					                    </div>
 | 
				
			||||||
                </div>
 | 
					                </div>
 | 
				
			||||||
                <div className="mx_CallView_voice_holdText">{onHoldText}</div>
 | 
					                {holdTransferContent}
 | 
				
			||||||
                {callControls}
 | 
					                {callControls}
 | 
				
			||||||
            </div>;
 | 
					            </div>;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
				
			|||||||
@ -68,24 +68,24 @@ export default class AutocompleteWrapperModel {
 | 
				
			|||||||
        this.updateCallback({close: true});
 | 
					        this.updateCallback({close: true});
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public async onTab(e: KeyboardEvent) {
 | 
					    /**
 | 
				
			||||||
 | 
					     * If there is no current autocompletion, start one and move to the first selection.
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public async startSelection() {
 | 
				
			||||||
        const acComponent = this.getAutocompleterComponent();
 | 
					        const acComponent = this.getAutocompleterComponent();
 | 
				
			||||||
 | 
					 | 
				
			||||||
        if (acComponent.countCompletions() === 0) {
 | 
					        if (acComponent.countCompletions() === 0) {
 | 
				
			||||||
            // Force completions to show for the text currently entered
 | 
					            // Force completions to show for the text currently entered
 | 
				
			||||||
            await acComponent.forceComplete();
 | 
					            await acComponent.forceComplete();
 | 
				
			||||||
            // Select the first item by moving "down"
 | 
					            // Select the first item by moving "down"
 | 
				
			||||||
            await acComponent.moveSelection(+1);
 | 
					            await acComponent.moveSelection(+1);
 | 
				
			||||||
        } else {
 | 
					 | 
				
			||||||
            await acComponent.moveSelection(e.shiftKey ? -1 : +1);
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public onUpArrow(e: KeyboardEvent) {
 | 
					    public selectPreviousSelection() {
 | 
				
			||||||
        this.getAutocompleterComponent().moveSelection(-1);
 | 
					        this.getAutocompleterComponent().moveSelection(-1);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public onDownArrow(e: KeyboardEvent) {
 | 
					    public selectNextSelection() {
 | 
				
			||||||
        this.getAutocompleterComponent().moveSelection(+1);
 | 
					        this.getAutocompleterComponent().moveSelection(+1);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -881,6 +881,8 @@
 | 
				
			|||||||
    "sends fireworks": "sends fireworks",
 | 
					    "sends fireworks": "sends fireworks",
 | 
				
			||||||
    "Sends the given message with snowfall": "Sends the given message with snowfall",
 | 
					    "Sends the given message with snowfall": "Sends the given message with snowfall",
 | 
				
			||||||
    "sends snowfall": "sends snowfall",
 | 
					    "sends snowfall": "sends snowfall",
 | 
				
			||||||
 | 
					    "unknown person": "unknown person",
 | 
				
			||||||
 | 
					    "Consulting with %(transferTarget)s. <a>Transfer to %(transferee)s</a>": "Consulting with %(transferTarget)s. <a>Transfer to %(transferee)s</a>",
 | 
				
			||||||
    "You held the call <a>Switch</a>": "You held the call <a>Switch</a>",
 | 
					    "You held the call <a>Switch</a>": "You held the call <a>Switch</a>",
 | 
				
			||||||
    "You held the call <a>Resume</a>": "You held the call <a>Resume</a>",
 | 
					    "You held the call <a>Resume</a>": "You held the call <a>Resume</a>",
 | 
				
			||||||
    "%(peerName)s held the call": "%(peerName)s held the call",
 | 
					    "%(peerName)s held the call": "%(peerName)s held the call",
 | 
				
			||||||
@ -1084,7 +1086,7 @@
 | 
				
			|||||||
    "Securely cache encrypted messages locally for them to appear in search results.": "Securely cache encrypted messages locally for them to appear in search results.",
 | 
					    "Securely cache encrypted messages locally for them to appear in search results.": "Securely cache encrypted messages locally for them to appear in search results.",
 | 
				
			||||||
    "%(brand)s is missing some components required for securely caching encrypted messages locally. If you'd like to experiment with this feature, build a custom %(brand)s Desktop with <nativeLink>search components added</nativeLink>.": "%(brand)s is missing some components required for securely caching encrypted messages locally. If you'd like to experiment with this feature, build a custom %(brand)s Desktop with <nativeLink>search components added</nativeLink>.",
 | 
					    "%(brand)s is missing some components required for securely caching encrypted messages locally. If you'd like to experiment with this feature, build a custom %(brand)s Desktop with <nativeLink>search components added</nativeLink>.": "%(brand)s is missing some components required for securely caching encrypted messages locally. If you'd like to experiment with this feature, build a custom %(brand)s Desktop with <nativeLink>search components added</nativeLink>.",
 | 
				
			||||||
    "%(brand)s can't securely cache encrypted messages locally while running in a web browser. Use <desktopLink>%(brand)s Desktop</desktopLink> for encrypted messages to appear in search results.": "%(brand)s can't securely cache encrypted messages locally while running in a web browser. Use <desktopLink>%(brand)s Desktop</desktopLink> for encrypted messages to appear in search results.",
 | 
					    "%(brand)s can't securely cache encrypted messages locally while running in a web browser. Use <desktopLink>%(brand)s Desktop</desktopLink> for encrypted messages to appear in search results.": "%(brand)s can't securely cache encrypted messages locally while running in a web browser. Use <desktopLink>%(brand)s Desktop</desktopLink> for encrypted messages to appear in search results.",
 | 
				
			||||||
    "Message search initialisation failed": "Message search initialisation failed",
 | 
					    "Message search initilisation failed": "Message search initilisation failed",
 | 
				
			||||||
    "Connecting to integration manager...": "Connecting to integration manager...",
 | 
					    "Connecting to integration manager...": "Connecting to integration manager...",
 | 
				
			||||||
    "Cannot connect to integration manager": "Cannot connect to integration manager",
 | 
					    "Cannot connect to integration manager": "Cannot connect to integration manager",
 | 
				
			||||||
    "The integration manager is offline or it cannot reach your homeserver.": "The integration manager is offline or it cannot reach your homeserver.",
 | 
					    "The integration manager is offline or it cannot reach your homeserver.": "The integration manager is offline or it cannot reach your homeserver.",
 | 
				
			||||||
@ -1576,6 +1578,7 @@
 | 
				
			|||||||
    "Start chatting": "Start chatting",
 | 
					    "Start chatting": "Start chatting",
 | 
				
			||||||
    "Do you want to join %(roomName)s?": "Do you want to join %(roomName)s?",
 | 
					    "Do you want to join %(roomName)s?": "Do you want to join %(roomName)s?",
 | 
				
			||||||
    "<userName/> invited you": "<userName/> invited you",
 | 
					    "<userName/> invited you": "<userName/> invited you",
 | 
				
			||||||
 | 
					    "Invite messages are hidden by default. Click to show the message.": "Invite messages are hidden by default. Click to show the message.",
 | 
				
			||||||
    "Reject": "Reject",
 | 
					    "Reject": "Reject",
 | 
				
			||||||
    "Reject & Ignore user": "Reject & Ignore user",
 | 
					    "Reject & Ignore user": "Reject & Ignore user",
 | 
				
			||||||
    "You're previewing %(roomName)s. Want to join it?": "You're previewing %(roomName)s. Want to join it?",
 | 
					    "You're previewing %(roomName)s. Want to join it?": "You're previewing %(roomName)s. Want to join it?",
 | 
				
			||||||
@ -2215,6 +2218,7 @@
 | 
				
			|||||||
    "Invite someone using their name, username (like <userId/>) or <a>share this room</a>.": "Invite someone using their name, username (like <userId/>) or <a>share this room</a>.",
 | 
					    "Invite someone using their name, username (like <userId/>) or <a>share this room</a>.": "Invite someone using their name, username (like <userId/>) or <a>share this room</a>.",
 | 
				
			||||||
    "Invited people will be able to read old messages.": "Invited people will be able to read old messages.",
 | 
					    "Invited people will be able to read old messages.": "Invited people will be able to read old messages.",
 | 
				
			||||||
    "Transfer": "Transfer",
 | 
					    "Transfer": "Transfer",
 | 
				
			||||||
 | 
					    "Consult first": "Consult first",
 | 
				
			||||||
    "a new master key signature": "a new master key signature",
 | 
					    "a new master key signature": "a new master key signature",
 | 
				
			||||||
    "a new cross-signing key signature": "a new cross-signing key signature",
 | 
					    "a new cross-signing key signature": "a new cross-signing key signature",
 | 
				
			||||||
    "a device cross-signing signature": "a device cross-signing signature",
 | 
					    "a device cross-signing signature": "a device cross-signing signature",
 | 
				
			||||||
@ -2305,6 +2309,10 @@
 | 
				
			|||||||
    "Use your preferred Matrix homeserver if you have one, or host your own.": "Use your preferred Matrix homeserver if you have one, or host your own.",
 | 
					    "Use your preferred Matrix homeserver if you have one, or host your own.": "Use your preferred Matrix homeserver if you have one, or host your own.",
 | 
				
			||||||
    "Learn more": "Learn more",
 | 
					    "Learn more": "Learn more",
 | 
				
			||||||
    "About homeservers": "About homeservers",
 | 
					    "About homeservers": "About homeservers",
 | 
				
			||||||
 | 
					    "Reset event store?": "Reset event store?",
 | 
				
			||||||
 | 
					    "You most likely do not want to reset your event index store": "You most likely do not want to reset your event index store",
 | 
				
			||||||
 | 
					    "If you do, please note that none of your messages will be deleted, but the search experience might be degraded for a few momentswhilst the index is recreated": "If you do, please note that none of your messages will be deleted, but the search experience might be degraded for a few momentswhilst the index is recreated",
 | 
				
			||||||
 | 
					    "Reset event store": "Reset event store",
 | 
				
			||||||
    "Sign out and remove encryption keys?": "Sign out and remove encryption keys?",
 | 
					    "Sign out and remove encryption keys?": "Sign out and remove encryption keys?",
 | 
				
			||||||
    "Clear Storage and Sign Out": "Clear Storage and Sign Out",
 | 
					    "Clear Storage and Sign Out": "Clear Storage and Sign Out",
 | 
				
			||||||
    "Send Logs": "Send Logs",
 | 
					    "Send Logs": "Send Logs",
 | 
				
			||||||
@ -2693,6 +2701,7 @@
 | 
				
			|||||||
    "Failed to send email": "Failed to send email",
 | 
					    "Failed to send email": "Failed to send email",
 | 
				
			||||||
    "The email address linked to your account must be entered.": "The email address linked to your account must be entered.",
 | 
					    "The email address linked to your account must be entered.": "The email address linked to your account must be entered.",
 | 
				
			||||||
    "A new password must be entered.": "A new password must be entered.",
 | 
					    "A new password must be entered.": "A new password must be entered.",
 | 
				
			||||||
 | 
					    "Please choose a strong password": "Please choose a strong password",
 | 
				
			||||||
    "New passwords must match each other.": "New passwords must match each other.",
 | 
					    "New passwords must match each other.": "New passwords must match each other.",
 | 
				
			||||||
    "Changing your password will reset any end-to-end encryption keys on all of your sessions, making encrypted chat history unreadable. Set up Key Backup or export your room keys from another session before resetting your password.": "Changing your password will reset any end-to-end encryption keys on all of your sessions, making encrypted chat history unreadable. Set up Key Backup or export your room keys from another session before resetting your password.",
 | 
					    "Changing your password will reset any end-to-end encryption keys on all of your sessions, making encrypted chat history unreadable. Set up Key Backup or export your room keys from another session before resetting your password.": "Changing your password will reset any end-to-end encryption keys on all of your sessions, making encrypted chat history unreadable. Set up Key Backup or export your room keys from another session before resetting your password.",
 | 
				
			||||||
    "New Password": "New Password",
 | 
					    "New Password": "New Password",
 | 
				
			||||||
 | 
				
			|||||||
@ -82,7 +82,7 @@ export class ListLayout {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    public get defaultVisibleTiles(): number {
 | 
					    public get defaultVisibleTiles(): number {
 | 
				
			||||||
        // This number is what "feels right", and mostly subject to design's opinion.
 | 
					        // This number is what "feels right", and mostly subject to design's opinion.
 | 
				
			||||||
        return 5;
 | 
					        return 8;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public tilesWithPadding(n: number, paddingPx: number): number {
 | 
					    public tilesWithPadding(n: number, paddingPx: number): number {
 | 
				
			||||||
 | 
				
			|||||||
@ -1,6 +1,5 @@
 | 
				
			|||||||
/*
 | 
					/*
 | 
				
			||||||
Copyright 2018, 2019 New Vector Ltd
 | 
					Copyright 2018-2021 The Matrix.org Foundation C.I.C.
 | 
				
			||||||
Copyright 2020 The Matrix.org Foundation C.I.C.
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
Licensed under the Apache License, Version 2.0 (the "License");
 | 
					Licensed under the Apache License, Version 2.0 (the "License");
 | 
				
			||||||
you may not use this file except in compliance with the License.
 | 
					you may not use this file except in compliance with the License.
 | 
				
			||||||
@ -15,27 +14,27 @@ See the License for the specific language governing permissions and
 | 
				
			|||||||
limitations under the License.
 | 
					limitations under the License.
 | 
				
			||||||
*/
 | 
					*/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { MatrixClient } from "matrix-js-sdk/src/client";
 | 
					import {MatrixClient} from "matrix-js-sdk/src/client";
 | 
				
			||||||
import SettingsStore from "../../settings/SettingsStore";
 | 
					import SettingsStore from "../../settings/SettingsStore";
 | 
				
			||||||
import { DefaultTagID, isCustomTag, OrderedDefaultTagIDs, RoomUpdateCause, TagID } from "./models";
 | 
					import {DefaultTagID, isCustomTag, OrderedDefaultTagIDs, RoomUpdateCause, TagID} from "./models";
 | 
				
			||||||
import { Room } from "matrix-js-sdk/src/models/room";
 | 
					import {Room} from "matrix-js-sdk/src/models/room";
 | 
				
			||||||
import { IListOrderingMap, ITagMap, ITagSortingMap, ListAlgorithm, SortAlgorithm } from "./algorithms/models";
 | 
					import {IListOrderingMap, ITagMap, ITagSortingMap, ListAlgorithm, SortAlgorithm} from "./algorithms/models";
 | 
				
			||||||
import { ActionPayload } from "../../dispatcher/payloads";
 | 
					import {ActionPayload} from "../../dispatcher/payloads";
 | 
				
			||||||
import defaultDispatcher from "../../dispatcher/dispatcher";
 | 
					import defaultDispatcher from "../../dispatcher/dispatcher";
 | 
				
			||||||
import { readReceiptChangeIsFor } from "../../utils/read-receipts";
 | 
					import {readReceiptChangeIsFor} from "../../utils/read-receipts";
 | 
				
			||||||
import { FILTER_CHANGED, IFilterCondition } from "./filters/IFilterCondition";
 | 
					import {FILTER_CHANGED, FilterKind, IFilterCondition} from "./filters/IFilterCondition";
 | 
				
			||||||
import { TagWatcher } from "./TagWatcher";
 | 
					import {TagWatcher} from "./TagWatcher";
 | 
				
			||||||
import RoomViewStore from "../RoomViewStore";
 | 
					import RoomViewStore from "../RoomViewStore";
 | 
				
			||||||
import { Algorithm, LIST_UPDATED_EVENT } from "./algorithms/Algorithm";
 | 
					import {Algorithm, LIST_UPDATED_EVENT} from "./algorithms/Algorithm";
 | 
				
			||||||
import { EffectiveMembership, getEffectiveMembership } from "../../utils/membership";
 | 
					import {EffectiveMembership, getEffectiveMembership} from "../../utils/membership";
 | 
				
			||||||
import { isNullOrUndefined } from "matrix-js-sdk/src/utils";
 | 
					import {isNullOrUndefined} from "matrix-js-sdk/src/utils";
 | 
				
			||||||
import RoomListLayoutStore from "./RoomListLayoutStore";
 | 
					import RoomListLayoutStore from "./RoomListLayoutStore";
 | 
				
			||||||
import { MarkedExecution } from "../../utils/MarkedExecution";
 | 
					import {MarkedExecution} from "../../utils/MarkedExecution";
 | 
				
			||||||
import { AsyncStoreWithClient } from "../AsyncStoreWithClient";
 | 
					import {AsyncStoreWithClient} from "../AsyncStoreWithClient";
 | 
				
			||||||
import { NameFilterCondition } from "./filters/NameFilterCondition";
 | 
					import {NameFilterCondition} from "./filters/NameFilterCondition";
 | 
				
			||||||
import { RoomNotificationStateStore } from "../notifications/RoomNotificationStateStore";
 | 
					import {RoomNotificationStateStore} from "../notifications/RoomNotificationStateStore";
 | 
				
			||||||
import { VisibilityProvider } from "./filters/VisibilityProvider";
 | 
					import {VisibilityProvider} from "./filters/VisibilityProvider";
 | 
				
			||||||
import { SpaceWatcher } from "./SpaceWatcher";
 | 
					import {SpaceWatcher} from "./SpaceWatcher";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
interface IState {
 | 
					interface IState {
 | 
				
			||||||
    tagsEnabled?: boolean;
 | 
					    tagsEnabled?: boolean;
 | 
				
			||||||
@ -57,6 +56,7 @@ export class RoomListStoreClass extends AsyncStoreWithClient<IState> {
 | 
				
			|||||||
    private initialListsGenerated = false;
 | 
					    private initialListsGenerated = false;
 | 
				
			||||||
    private algorithm = new Algorithm();
 | 
					    private algorithm = new Algorithm();
 | 
				
			||||||
    private filterConditions: IFilterCondition[] = [];
 | 
					    private filterConditions: IFilterCondition[] = [];
 | 
				
			||||||
 | 
					    private prefilterConditions: IFilterCondition[] = [];
 | 
				
			||||||
    private tagWatcher: TagWatcher;
 | 
					    private tagWatcher: TagWatcher;
 | 
				
			||||||
    private spaceWatcher: SpaceWatcher;
 | 
					    private spaceWatcher: SpaceWatcher;
 | 
				
			||||||
    private updateFn = new MarkedExecution(() => {
 | 
					    private updateFn = new MarkedExecution(() => {
 | 
				
			||||||
@ -104,6 +104,7 @@ export class RoomListStoreClass extends AsyncStoreWithClient<IState> {
 | 
				
			|||||||
    public async resetStore() {
 | 
					    public async resetStore() {
 | 
				
			||||||
        await this.reset();
 | 
					        await this.reset();
 | 
				
			||||||
        this.filterConditions = [];
 | 
					        this.filterConditions = [];
 | 
				
			||||||
 | 
					        this.prefilterConditions = [];
 | 
				
			||||||
        this.initialListsGenerated = false;
 | 
					        this.initialListsGenerated = false;
 | 
				
			||||||
        this.setupWatchers();
 | 
					        this.setupWatchers();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -435,6 +436,39 @@ export class RoomListStoreClass extends AsyncStoreWithClient<IState> {
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private async recalculatePrefiltering() {
 | 
				
			||||||
 | 
					        if (!this.algorithm) return;
 | 
				
			||||||
 | 
					        if (!this.algorithm.hasTagSortingMap) return; // we're still loading
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (SettingsStore.getValue("advancedRoomListLogging")) {
 | 
				
			||||||
 | 
					            // TODO: Remove debug: https://github.com/vector-im/element-web/issues/14602
 | 
				
			||||||
 | 
					            console.log("Calculating new prefiltered room list");
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Inhibit updates because we're about to lie heavily to the algorithm
 | 
				
			||||||
 | 
					        this.algorithm.updatesInhibited = true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Figure out which rooms are about to be valid, and the state of affairs
 | 
				
			||||||
 | 
					        const rooms = this.getPlausibleRooms();
 | 
				
			||||||
 | 
					        const currentSticky = this.algorithm.stickyRoom;
 | 
				
			||||||
 | 
					        const stickyIsStillPresent = currentSticky && rooms.includes(currentSticky);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Reset the sticky room before resetting the known rooms so the algorithm
 | 
				
			||||||
 | 
					        // doesn't freak out.
 | 
				
			||||||
 | 
					        await this.algorithm.setStickyRoom(null);
 | 
				
			||||||
 | 
					        await this.algorithm.setKnownRooms(rooms);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Set the sticky room back, if needed, now that we have updated the store.
 | 
				
			||||||
 | 
					        // This will use relative stickyness to the new room set.
 | 
				
			||||||
 | 
					        if (stickyIsStillPresent) {
 | 
				
			||||||
 | 
					            await this.algorithm.setStickyRoom(currentSticky);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Finally, mark an update and resume updates from the algorithm
 | 
				
			||||||
 | 
					        this.updateFn.mark();
 | 
				
			||||||
 | 
					        this.algorithm.updatesInhibited = false;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public async setTagSorting(tagId: TagID, sort: SortAlgorithm) {
 | 
					    public async setTagSorting(tagId: TagID, sort: SortAlgorithm) {
 | 
				
			||||||
        await this.setAndPersistTagSorting(tagId, sort);
 | 
					        await this.setAndPersistTagSorting(tagId, sort);
 | 
				
			||||||
        this.updateFn.trigger();
 | 
					        this.updateFn.trigger();
 | 
				
			||||||
@ -557,6 +591,34 @@ export class RoomListStoreClass extends AsyncStoreWithClient<IState> {
 | 
				
			|||||||
        this.updateFn.trigger();
 | 
					        this.updateFn.trigger();
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private onPrefilterUpdated = async () => {
 | 
				
			||||||
 | 
					        await this.recalculatePrefiltering();
 | 
				
			||||||
 | 
					        this.updateFn.trigger();
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private getPlausibleRooms(): Room[] {
 | 
				
			||||||
 | 
					        if (!this.matrixClient) return [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let rooms = [
 | 
				
			||||||
 | 
					            ...this.matrixClient.getVisibleRooms(),
 | 
				
			||||||
 | 
					            // also show space invites in the room list
 | 
				
			||||||
 | 
					            ...this.matrixClient.getRooms().filter(r => r.isSpaceRoom() && r.getMyMembership() === "invite"),
 | 
				
			||||||
 | 
					        ].filter(r => VisibilityProvider.instance.isRoomVisible(r));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (this.prefilterConditions.length > 0) {
 | 
				
			||||||
 | 
					            rooms = rooms.filter(r => {
 | 
				
			||||||
 | 
					                for (const filter of this.prefilterConditions) {
 | 
				
			||||||
 | 
					                    if (!filter.isVisible(r)) {
 | 
				
			||||||
 | 
					                        return false;
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                return true;
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return rooms;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * Regenerates the room whole room list, discarding any previous results.
 | 
					     * Regenerates the room whole room list, discarding any previous results.
 | 
				
			||||||
     *
 | 
					     *
 | 
				
			||||||
@ -568,11 +630,7 @@ export class RoomListStoreClass extends AsyncStoreWithClient<IState> {
 | 
				
			|||||||
    public async regenerateAllLists({trigger = true}) {
 | 
					    public async regenerateAllLists({trigger = true}) {
 | 
				
			||||||
        console.warn("Regenerating all room lists");
 | 
					        console.warn("Regenerating all room lists");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const rooms = [
 | 
					        const rooms = this.getPlausibleRooms();
 | 
				
			||||||
            ...this.matrixClient.getVisibleRooms(),
 | 
					 | 
				
			||||||
            // also show space invites in the room list
 | 
					 | 
				
			||||||
            ...this.matrixClient.getRooms().filter(r => r.isSpaceRoom() && r.getMyMembership() === "invite"),
 | 
					 | 
				
			||||||
        ].filter(r => VisibilityProvider.instance.isRoomVisible(r));
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const customTags = new Set<TagID>();
 | 
					        const customTags = new Set<TagID>();
 | 
				
			||||||
        if (this.state.tagsEnabled) {
 | 
					        if (this.state.tagsEnabled) {
 | 
				
			||||||
@ -601,24 +659,44 @@ export class RoomListStoreClass extends AsyncStoreWithClient<IState> {
 | 
				
			|||||||
        if (trigger) this.updateFn.trigger();
 | 
					        if (trigger) this.updateFn.trigger();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Adds a filter condition to the room list store. Filters may be applied async,
 | 
				
			||||||
 | 
					     * and thus might not cause an update to the store immediately.
 | 
				
			||||||
 | 
					     * @param {IFilterCondition} filter The filter condition to add.
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
    public addFilter(filter: IFilterCondition): void {
 | 
					    public addFilter(filter: IFilterCondition): void {
 | 
				
			||||||
        if (SettingsStore.getValue("advancedRoomListLogging")) {
 | 
					        if (SettingsStore.getValue("advancedRoomListLogging")) {
 | 
				
			||||||
            // TODO: Remove debug: https://github.com/vector-im/element-web/issues/14602
 | 
					            // TODO: Remove debug: https://github.com/vector-im/element-web/issues/14602
 | 
				
			||||||
            console.log("Adding filter condition:", filter);
 | 
					            console.log("Adding filter condition:", filter);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        this.filterConditions.push(filter);
 | 
					        let promise = Promise.resolve();
 | 
				
			||||||
        if (this.algorithm) {
 | 
					        if (filter.kind === FilterKind.Prefilter) {
 | 
				
			||||||
            this.algorithm.addFilterCondition(filter);
 | 
					            filter.on(FILTER_CHANGED, this.onPrefilterUpdated);
 | 
				
			||||||
 | 
					            this.prefilterConditions.push(filter);
 | 
				
			||||||
 | 
					            promise = this.recalculatePrefiltering();
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            this.filterConditions.push(filter);
 | 
				
			||||||
 | 
					            if (this.algorithm) {
 | 
				
			||||||
 | 
					                this.algorithm.addFilterCondition(filter);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        this.updateFn.trigger();
 | 
					        promise.then(() => this.updateFn.trigger());
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Removes a filter condition from the room list store. If the filter was
 | 
				
			||||||
 | 
					     * not previously added to the room list store, this will no-op. The effects
 | 
				
			||||||
 | 
					     * of removing a filter may be applied async and therefore might not cause
 | 
				
			||||||
 | 
					     * an update right away.
 | 
				
			||||||
 | 
					     * @param {IFilterCondition} filter The filter condition to remove.
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
    public removeFilter(filter: IFilterCondition): void {
 | 
					    public removeFilter(filter: IFilterCondition): void {
 | 
				
			||||||
        if (SettingsStore.getValue("advancedRoomListLogging")) {
 | 
					        if (SettingsStore.getValue("advancedRoomListLogging")) {
 | 
				
			||||||
            // TODO: Remove debug: https://github.com/vector-im/element-web/issues/14602
 | 
					            // TODO: Remove debug: https://github.com/vector-im/element-web/issues/14602
 | 
				
			||||||
            console.log("Removing filter condition:", filter);
 | 
					            console.log("Removing filter condition:", filter);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        const idx = this.filterConditions.indexOf(filter);
 | 
					        let promise = Promise.resolve();
 | 
				
			||||||
 | 
					        let idx = this.filterConditions.indexOf(filter);
 | 
				
			||||||
        if (idx >= 0) {
 | 
					        if (idx >= 0) {
 | 
				
			||||||
            this.filterConditions.splice(idx, 1);
 | 
					            this.filterConditions.splice(idx, 1);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -626,7 +704,13 @@ export class RoomListStoreClass extends AsyncStoreWithClient<IState> {
 | 
				
			|||||||
                this.algorithm.removeFilterCondition(filter);
 | 
					                this.algorithm.removeFilterCondition(filter);
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        this.updateFn.trigger();
 | 
					        idx = this.prefilterConditions.indexOf(filter);
 | 
				
			||||||
 | 
					        if (idx >= 0) {
 | 
				
			||||||
 | 
					            filter.off(FILTER_CHANGED, this.onPrefilterUpdated);
 | 
				
			||||||
 | 
					            this.prefilterConditions.splice(idx, 1);
 | 
				
			||||||
 | 
					            promise = this.recalculatePrefiltering();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        promise.then(() => this.updateFn.trigger());
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
 | 
				
			|||||||
@ -1,5 +1,5 @@
 | 
				
			|||||||
/*
 | 
					/*
 | 
				
			||||||
Copyright 2020 The Matrix.org Foundation C.I.C.
 | 
					Copyright 2020, 2021 The Matrix.org Foundation C.I.C.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Licensed under the Apache License, Version 2.0 (the "License");
 | 
					Licensed under the Apache License, Version 2.0 (the "License");
 | 
				
			||||||
you may not use this file except in compliance with the License.
 | 
					you may not use this file except in compliance with the License.
 | 
				
			||||||
@ -18,8 +18,7 @@ import { Room } from "matrix-js-sdk/src/models/room";
 | 
				
			|||||||
import { isNullOrUndefined } from "matrix-js-sdk/src/utils";
 | 
					import { isNullOrUndefined } from "matrix-js-sdk/src/utils";
 | 
				
			||||||
import DMRoomMap from "../../../utils/DMRoomMap";
 | 
					import DMRoomMap from "../../../utils/DMRoomMap";
 | 
				
			||||||
import { EventEmitter } from "events";
 | 
					import { EventEmitter } from "events";
 | 
				
			||||||
import { arrayDiff, arrayHasDiff, ArrayUtil } from "../../../utils/arrays";
 | 
					import { arrayDiff, arrayHasDiff } from "../../../utils/arrays";
 | 
				
			||||||
import { getEnumValues } from "../../../utils/enums";
 | 
					 | 
				
			||||||
import { DefaultTagID, RoomUpdateCause, TagID } from "../models";
 | 
					import { DefaultTagID, RoomUpdateCause, TagID } from "../models";
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
    IListOrderingMap,
 | 
					    IListOrderingMap,
 | 
				
			||||||
@ -29,7 +28,7 @@ import {
 | 
				
			|||||||
    ListAlgorithm,
 | 
					    ListAlgorithm,
 | 
				
			||||||
    SortAlgorithm,
 | 
					    SortAlgorithm,
 | 
				
			||||||
} from "./models";
 | 
					} from "./models";
 | 
				
			||||||
import { FILTER_CHANGED, FilterPriority, IFilterCondition } from "../filters/IFilterCondition";
 | 
					import { FILTER_CHANGED, IFilterCondition } from "../filters/IFilterCondition";
 | 
				
			||||||
import { EffectiveMembership, getEffectiveMembership, splitRoomsByMembership } from "../../../utils/membership";
 | 
					import { EffectiveMembership, getEffectiveMembership, splitRoomsByMembership } from "../../../utils/membership";
 | 
				
			||||||
import { OrderingAlgorithm } from "./list-ordering/OrderingAlgorithm";
 | 
					import { OrderingAlgorithm } from "./list-ordering/OrderingAlgorithm";
 | 
				
			||||||
import { getListAlgorithmInstance } from "./list-ordering";
 | 
					import { getListAlgorithmInstance } from "./list-ordering";
 | 
				
			||||||
@ -79,6 +78,11 @@ export class Algorithm extends EventEmitter {
 | 
				
			|||||||
    private allowedByFilter: Map<IFilterCondition, Room[]> = new Map<IFilterCondition, Room[]>();
 | 
					    private allowedByFilter: Map<IFilterCondition, Room[]> = new Map<IFilterCondition, Room[]>();
 | 
				
			||||||
    private allowedRoomsByFilters: Set<Room> = new Set<Room>();
 | 
					    private allowedRoomsByFilters: Set<Room> = new Set<Room>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Set to true to suspend emissions of algorithm updates.
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public updatesInhibited = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public constructor() {
 | 
					    public constructor() {
 | 
				
			||||||
        super();
 | 
					        super();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@ -87,6 +91,14 @@ export class Algorithm extends EventEmitter {
 | 
				
			|||||||
        return this._stickyRoom ? this._stickyRoom.room : null;
 | 
					        return this._stickyRoom ? this._stickyRoom.room : null;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public get knownRooms(): Room[] {
 | 
				
			||||||
 | 
					        return this.rooms;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public get hasTagSortingMap(): boolean {
 | 
				
			||||||
 | 
					        return !!this.sortAlgorithms;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    protected get hasFilters(): boolean {
 | 
					    protected get hasFilters(): boolean {
 | 
				
			||||||
        return this.allowedByFilter.size > 0;
 | 
					        return this.allowedByFilter.size > 0;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@ -164,7 +176,7 @@ export class Algorithm extends EventEmitter {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
            // If we removed the last filter, tell consumers that we've "updated" our filtered
 | 
					            // If we removed the last filter, tell consumers that we've "updated" our filtered
 | 
				
			||||||
            // view. This will trick them into getting the complete room list.
 | 
					            // view. This will trick them into getting the complete room list.
 | 
				
			||||||
            if (!this.hasFilters) {
 | 
					            if (!this.hasFilters && !this.updatesInhibited) {
 | 
				
			||||||
                this.emit(LIST_UPDATED_EVENT);
 | 
					                this.emit(LIST_UPDATED_EVENT);
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
@ -174,6 +186,7 @@ export class Algorithm extends EventEmitter {
 | 
				
			|||||||
        await this.recalculateFilteredRooms();
 | 
					        await this.recalculateFilteredRooms();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // re-emit the update so the list store can fire an off-cycle update if needed
 | 
					        // re-emit the update so the list store can fire an off-cycle update if needed
 | 
				
			||||||
 | 
					        if (this.updatesInhibited) return;
 | 
				
			||||||
        this.emit(FILTER_CHANGED);
 | 
					        this.emit(FILTER_CHANGED);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -299,6 +312,7 @@ export class Algorithm extends EventEmitter {
 | 
				
			|||||||
        this.recalculateStickyRoom();
 | 
					        this.recalculateStickyRoom();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // Finally, trigger an update
 | 
					        // Finally, trigger an update
 | 
				
			||||||
 | 
					        if (this.updatesInhibited) return;
 | 
				
			||||||
        this.emit(LIST_UPDATED_EVENT);
 | 
					        this.emit(LIST_UPDATED_EVENT);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -309,10 +323,6 @@ export class Algorithm extends EventEmitter {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        console.warn("Recalculating filtered room list");
 | 
					        console.warn("Recalculating filtered room list");
 | 
				
			||||||
        const filters = Array.from(this.allowedByFilter.keys());
 | 
					        const filters = Array.from(this.allowedByFilter.keys());
 | 
				
			||||||
        const orderedFilters = new ArrayUtil(filters)
 | 
					 | 
				
			||||||
            .groupBy(f => f.relativePriority)
 | 
					 | 
				
			||||||
            .orderBy(getEnumValues(FilterPriority))
 | 
					 | 
				
			||||||
            .value;
 | 
					 | 
				
			||||||
        const newMap: ITagMap = {};
 | 
					        const newMap: ITagMap = {};
 | 
				
			||||||
        for (const tagId of Object.keys(this.cachedRooms)) {
 | 
					        for (const tagId of Object.keys(this.cachedRooms)) {
 | 
				
			||||||
            // Cheaply clone the rooms so we can more easily do operations on the list.
 | 
					            // Cheaply clone the rooms so we can more easily do operations on the list.
 | 
				
			||||||
@ -320,18 +330,9 @@ export class Algorithm extends EventEmitter {
 | 
				
			|||||||
            // to the rooms we know will be deduped by the Set.
 | 
					            // to the rooms we know will be deduped by the Set.
 | 
				
			||||||
            const rooms = this.cachedRooms[tagId].map(r => r); // cheap clone
 | 
					            const rooms = this.cachedRooms[tagId].map(r => r); // cheap clone
 | 
				
			||||||
            this.tryInsertStickyRoomToFilterSet(rooms, tagId);
 | 
					            this.tryInsertStickyRoomToFilterSet(rooms, tagId);
 | 
				
			||||||
            let remainingRooms = rooms.map(r => r);
 | 
					            const remainingRooms = rooms.map(r => r);
 | 
				
			||||||
            let allowedRoomsInThisTag = [];
 | 
					            const allowedRoomsInThisTag = [];
 | 
				
			||||||
            let lastFilterPriority = orderedFilters[0].relativePriority;
 | 
					            for (const filter of filters) {
 | 
				
			||||||
            for (const filter of orderedFilters) {
 | 
					 | 
				
			||||||
                if (filter.relativePriority !== lastFilterPriority) {
 | 
					 | 
				
			||||||
                    // Every time the filter changes priority, we want more specific filtering.
 | 
					 | 
				
			||||||
                    // To accomplish that, reset the variables to make it look like the process
 | 
					 | 
				
			||||||
                    // has started over, but using the filtered rooms as the seed.
 | 
					 | 
				
			||||||
                    remainingRooms = allowedRoomsInThisTag;
 | 
					 | 
				
			||||||
                    allowedRoomsInThisTag = [];
 | 
					 | 
				
			||||||
                    lastFilterPriority = filter.relativePriority;
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
                const filteredRooms = remainingRooms.filter(r => filter.isVisible(r));
 | 
					                const filteredRooms = remainingRooms.filter(r => filter.isVisible(r));
 | 
				
			||||||
                for (const room of filteredRooms) {
 | 
					                for (const room of filteredRooms) {
 | 
				
			||||||
                    const idx = remainingRooms.indexOf(room);
 | 
					                    const idx = remainingRooms.indexOf(room);
 | 
				
			||||||
@ -350,6 +351,7 @@ export class Algorithm extends EventEmitter {
 | 
				
			|||||||
        const allowedRooms = Object.values(newMap).reduce((rv, v) => { rv.push(...v); return rv; }, <Room[]>[]);
 | 
					        const allowedRooms = Object.values(newMap).reduce((rv, v) => { rv.push(...v); return rv; }, <Room[]>[]);
 | 
				
			||||||
        this.allowedRoomsByFilters = new Set(allowedRooms);
 | 
					        this.allowedRoomsByFilters = new Set(allowedRooms);
 | 
				
			||||||
        this.filteredRooms = newMap;
 | 
					        this.filteredRooms = newMap;
 | 
				
			||||||
 | 
					        if (this.updatesInhibited) return;
 | 
				
			||||||
        this.emit(LIST_UPDATED_EVENT);
 | 
					        this.emit(LIST_UPDATED_EVENT);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -404,6 +406,7 @@ export class Algorithm extends EventEmitter {
 | 
				
			|||||||
            if (!!this._cachedStickyRooms) {
 | 
					            if (!!this._cachedStickyRooms) {
 | 
				
			||||||
                // Clear the cache if we won't be needing it
 | 
					                // Clear the cache if we won't be needing it
 | 
				
			||||||
                this._cachedStickyRooms = null;
 | 
					                this._cachedStickyRooms = null;
 | 
				
			||||||
 | 
					                if (this.updatesInhibited) return;
 | 
				
			||||||
                this.emit(LIST_UPDATED_EVENT);
 | 
					                this.emit(LIST_UPDATED_EVENT);
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            return;
 | 
					            return;
 | 
				
			||||||
@ -446,6 +449,7 @@ export class Algorithm extends EventEmitter {
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // Finally, trigger an update
 | 
					        // Finally, trigger an update
 | 
				
			||||||
 | 
					        if (this.updatesInhibited) return;
 | 
				
			||||||
        this.emit(LIST_UPDATED_EVENT);
 | 
					        this.emit(LIST_UPDATED_EVENT);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -512,7 +516,12 @@ export class Algorithm extends EventEmitter {
 | 
				
			|||||||
        if (isNullOrUndefined(rooms)) throw new Error(`Array of rooms cannot be null`);
 | 
					        if (isNullOrUndefined(rooms)) throw new Error(`Array of rooms cannot be null`);
 | 
				
			||||||
        if (!this.sortAlgorithms) throw new Error(`Cannot set known rooms without a tag sorting map`);
 | 
					        if (!this.sortAlgorithms) throw new Error(`Cannot set known rooms without a tag sorting map`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        console.warn("Resetting known rooms, initiating regeneration");
 | 
					        if (!this.updatesInhibited) {
 | 
				
			||||||
 | 
					            // We only log this if we're expecting to be publishing updates, which means that
 | 
				
			||||||
 | 
					            // this could be an unexpected invocation. If we're inhibited, then this is probably
 | 
				
			||||||
 | 
					            // an intentional invocation.
 | 
				
			||||||
 | 
					            console.warn("Resetting known rooms, initiating regeneration");
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // Before we go any further we need to clear (but remember) the sticky room to
 | 
					        // Before we go any further we need to clear (but remember) the sticky room to
 | 
				
			||||||
        // avoid accidentally duplicating it in the list.
 | 
					        // avoid accidentally duplicating it in the list.
 | 
				
			||||||
 | 
				
			|||||||
@ -1,5 +1,5 @@
 | 
				
			|||||||
/*
 | 
					/*
 | 
				
			||||||
Copyright 2020 The Matrix.org Foundation C.I.C.
 | 
					Copyright 2020, 2021 The Matrix.org Foundation C.I.C.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Licensed under the Apache License, Version 2.0 (the "License");
 | 
					Licensed under the Apache License, Version 2.0 (the "License");
 | 
				
			||||||
you may not use this file except in compliance with the License.
 | 
					you may not use this file except in compliance with the License.
 | 
				
			||||||
@ -15,7 +15,7 @@ limitations under the License.
 | 
				
			|||||||
*/
 | 
					*/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { Room } from "matrix-js-sdk/src/models/room";
 | 
					import { Room } from "matrix-js-sdk/src/models/room";
 | 
				
			||||||
import { FILTER_CHANGED, FilterPriority, IFilterCondition } from "./IFilterCondition";
 | 
					import { FILTER_CHANGED, FilterKind, IFilterCondition } from "./IFilterCondition";
 | 
				
			||||||
import { Group } from "matrix-js-sdk/src/models/group";
 | 
					import { Group } from "matrix-js-sdk/src/models/group";
 | 
				
			||||||
import { EventEmitter } from "events";
 | 
					import { EventEmitter } from "events";
 | 
				
			||||||
import GroupStore from "../../GroupStore";
 | 
					import GroupStore from "../../GroupStore";
 | 
				
			||||||
@ -39,9 +39,8 @@ export class CommunityFilterCondition extends EventEmitter implements IFilterCon
 | 
				
			|||||||
        this.onStoreUpdate(); // trigger a false update to seed the store
 | 
					        this.onStoreUpdate(); // trigger a false update to seed the store
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public get relativePriority(): FilterPriority {
 | 
					    public get kind(): FilterKind {
 | 
				
			||||||
        // Lowest priority so we can coarsely find rooms.
 | 
					        return FilterKind.Prefilter;
 | 
				
			||||||
        return FilterPriority.Lowest;
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public isVisible(room: Room): boolean {
 | 
					    public isVisible(room: Room): boolean {
 | 
				
			||||||
 | 
				
			|||||||
@ -1,5 +1,5 @@
 | 
				
			|||||||
/*
 | 
					/*
 | 
				
			||||||
Copyright 2020 The Matrix.org Foundation C.I.C.
 | 
					Copyright 2020, 2021 The Matrix.org Foundation C.I.C.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Licensed under the Apache License, Version 2.0 (the "License");
 | 
					Licensed under the Apache License, Version 2.0 (the "License");
 | 
				
			||||||
you may not use this file except in compliance with the License.
 | 
					you may not use this file except in compliance with the License.
 | 
				
			||||||
@ -19,10 +19,19 @@ import { EventEmitter } from "events";
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
export const FILTER_CHANGED = "filter_changed";
 | 
					export const FILTER_CHANGED = "filter_changed";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export enum FilterPriority {
 | 
					export enum FilterKind {
 | 
				
			||||||
    Lowest,
 | 
					    /**
 | 
				
			||||||
    // in the middle would be Low, Normal, and High if we had a need
 | 
					     * A prefilter is one which coarsely determines which rooms are
 | 
				
			||||||
    Highest,
 | 
					     * available for runtime filtering/rendering. Typically this will
 | 
				
			||||||
 | 
					     * be things like Space selection.
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    Prefilter,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Runtime filters operate on the data set exposed by prefilters.
 | 
				
			||||||
 | 
					     * Typically these are dynamic values like room name searching.
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    Runtime,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
@ -39,10 +48,9 @@ export enum FilterPriority {
 | 
				
			|||||||
 */
 | 
					 */
 | 
				
			||||||
export interface IFilterCondition extends EventEmitter {
 | 
					export interface IFilterCondition extends EventEmitter {
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * The relative priority that this filter should be applied with.
 | 
					     * The kind of filter this presents.
 | 
				
			||||||
     * Lower priorities get applied first.
 | 
					 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    relativePriority: FilterPriority;
 | 
					    kind: FilterKind;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * Determines if a given room should be visible under this
 | 
					     * Determines if a given room should be visible under this
 | 
				
			||||||
 | 
				
			|||||||
@ -1,5 +1,5 @@
 | 
				
			|||||||
/*
 | 
					/*
 | 
				
			||||||
Copyright 2020 The Matrix.org Foundation C.I.C.
 | 
					Copyright 2020, 2021 The Matrix.org Foundation C.I.C.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Licensed under the Apache License, Version 2.0 (the "License");
 | 
					Licensed under the Apache License, Version 2.0 (the "License");
 | 
				
			||||||
you may not use this file except in compliance with the License.
 | 
					you may not use this file except in compliance with the License.
 | 
				
			||||||
@ -15,7 +15,7 @@ limitations under the License.
 | 
				
			|||||||
*/
 | 
					*/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { Room } from "matrix-js-sdk/src/models/room";
 | 
					import { Room } from "matrix-js-sdk/src/models/room";
 | 
				
			||||||
import { FILTER_CHANGED, FilterPriority, IFilterCondition } from "./IFilterCondition";
 | 
					import { FILTER_CHANGED, FilterKind, IFilterCondition } from "./IFilterCondition";
 | 
				
			||||||
import { EventEmitter } from "events";
 | 
					import { EventEmitter } from "events";
 | 
				
			||||||
import { removeHiddenChars } from "matrix-js-sdk/src/utils";
 | 
					import { removeHiddenChars } from "matrix-js-sdk/src/utils";
 | 
				
			||||||
import { throttle } from "lodash";
 | 
					import { throttle } from "lodash";
 | 
				
			||||||
@ -31,9 +31,8 @@ export class NameFilterCondition extends EventEmitter implements IFilterConditio
 | 
				
			|||||||
        super();
 | 
					        super();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public get relativePriority(): FilterPriority {
 | 
					    public get kind(): FilterKind {
 | 
				
			||||||
        // We want this one to be at the highest priority so it can search within other filters.
 | 
					        return FilterKind.Runtime;
 | 
				
			||||||
        return FilterPriority.Highest;
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public get search(): string {
 | 
					    public get search(): string {
 | 
				
			||||||
 | 
				
			|||||||
@ -17,7 +17,7 @@ limitations under the License.
 | 
				
			|||||||
import { EventEmitter } from "events";
 | 
					import { EventEmitter } from "events";
 | 
				
			||||||
import { Room } from "matrix-js-sdk/src/models/room";
 | 
					import { Room } from "matrix-js-sdk/src/models/room";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { FILTER_CHANGED, FilterPriority, IFilterCondition } from "./IFilterCondition";
 | 
					import { FILTER_CHANGED, FilterKind, IFilterCondition } from "./IFilterCondition";
 | 
				
			||||||
import { IDestroyable } from "../../../utils/IDestroyable";
 | 
					import { IDestroyable } from "../../../utils/IDestroyable";
 | 
				
			||||||
import SpaceStore, {HOME_SPACE} from "../../SpaceStore";
 | 
					import SpaceStore, {HOME_SPACE} from "../../SpaceStore";
 | 
				
			||||||
import { setHasDiff } from "../../../utils/sets";
 | 
					import { setHasDiff } from "../../../utils/sets";
 | 
				
			||||||
@ -32,9 +32,8 @@ export class SpaceFilterCondition extends EventEmitter implements IFilterConditi
 | 
				
			|||||||
    private roomIds = new Set<Room>();
 | 
					    private roomIds = new Set<Room>();
 | 
				
			||||||
    private space: Room = null;
 | 
					    private space: Room = null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public get relativePriority(): FilterPriority {
 | 
					    public get kind(): FilterKind {
 | 
				
			||||||
        // Lowest priority so we can coarsely find rooms.
 | 
					        return FilterKind.Prefilter;
 | 
				
			||||||
        return FilterPriority.Lowest;
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public isVisible(room: Room): boolean {
 | 
					    public isVisible(room: Room): boolean {
 | 
				
			||||||
@ -46,12 +45,7 @@ export class SpaceFilterCondition extends EventEmitter implements IFilterConditi
 | 
				
			|||||||
        this.roomIds = SpaceStore.instance.getSpaceFilteredRoomIds(this.space);
 | 
					        this.roomIds = SpaceStore.instance.getSpaceFilteredRoomIds(this.space);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (setHasDiff(beforeRoomIds, this.roomIds)) {
 | 
					        if (setHasDiff(beforeRoomIds, this.roomIds)) {
 | 
				
			||||||
            // XXX: Room List Store has a bug where rooms which are synced after the filter is set
 | 
					 | 
				
			||||||
            // are excluded from the filter, this is a workaround for it.
 | 
					 | 
				
			||||||
            this.emit(FILTER_CHANGED);
 | 
					            this.emit(FILTER_CHANGED);
 | 
				
			||||||
            setTimeout(() => {
 | 
					 | 
				
			||||||
                this.emit(FILTER_CHANGED);
 | 
					 | 
				
			||||||
            }, 500);
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -183,18 +183,4 @@ describe('QueryMatcher', function() {
 | 
				
			|||||||
        expect(results.length).toBe(1);
 | 
					        expect(results.length).toBe(1);
 | 
				
			||||||
        expect(results[0].name).toBe('bob');
 | 
					        expect(results[0].name).toBe('bob');
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					 | 
				
			||||||
    it('Matches only by prefix with shouldMatchPrefix on', function() {
 | 
					 | 
				
			||||||
        const qm = new QueryMatcher([
 | 
					 | 
				
			||||||
            {name: "Victoria"},
 | 
					 | 
				
			||||||
            {name: "Tori"},
 | 
					 | 
				
			||||||
        ], {
 | 
					 | 
				
			||||||
            keys: ["name"],
 | 
					 | 
				
			||||||
            shouldMatchPrefix: true,
 | 
					 | 
				
			||||||
         });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        const results = qm.match('tori');
 | 
					 | 
				
			||||||
        expect(results.length).toBe(1);
 | 
					 | 
				
			||||||
        expect(results[0].name).toBe('Tori');
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
				
			|||||||
@ -296,6 +296,11 @@ describe('RoomList', () => {
 | 
				
			|||||||
            GroupStore._notifyListeners();
 | 
					            GroupStore._notifyListeners();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            await waitForRoomListStoreUpdate();
 | 
					            await waitForRoomListStoreUpdate();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // XXX: Even though the store updated, it can take a bit before the update makes
 | 
				
			||||||
 | 
					            // it to the components. This gives it plenty of time to figure out what to do.
 | 
				
			||||||
 | 
					            await (new Promise(resolve => setTimeout(resolve, 500)));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            expectRoomInSubList(otherRoom, (s) => s.props.tagId === DefaultTagID.Untagged);
 | 
					            expectRoomInSubList(otherRoom, (s) => s.props.tagId === DefaultTagID.Untagged);
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user