diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index 3b67db374c..c265ed4bb9 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -426,9 +426,6 @@
"Prepends ( ͡° ͜ʖ ͡°) to a plain-text message": "Prepends ( ͡° ͜ʖ ͡°) to a plain-text message",
"Sends a message as plain text, without interpreting it as markdown": "Sends a message as plain text, without interpreting it as markdown",
"Sends a message as html, without interpreting it as markdown": "Sends a message as html, without interpreting it as markdown",
- "Searches DuckDuckGo for results": "Searches DuckDuckGo for results",
- "/ddg is not a command": "/ddg is not a command",
- "To use it, just wait for autocomplete results to load and tab through them.": "To use it, just wait for autocomplete results to load and tab through them.",
"Upgrades a room to a new version": "Upgrades a room to a new version",
"You do not have the required permissions to use this command.": "You do not have the required permissions to use this command.",
"Changes your display nickname": "Changes your display nickname",
@@ -811,6 +808,7 @@
"Render LaTeX maths in messages": "Render LaTeX maths in messages",
"Communities v2 prototypes. Requires compatible homeserver. Highly experimental - use with caution.": "Communities v2 prototypes. Requires compatible homeserver. Highly experimental - use with caution.",
"Message Pinning": "Message Pinning",
+ "Threaded messaging": "Threaded messaging",
"Custom user status messages": "Custom user status messages",
"Group & filter rooms by custom tags (refresh to apply changes)": "Group & filter rooms by custom tags (refresh to apply changes)",
"Render simple counters in room header": "Render simple counters in room header",
@@ -1808,6 +1806,7 @@
"%(count)s people|other": "%(count)s people",
"%(count)s people|one": "%(count)s person",
"Show files": "Show files",
+ "Show threads": "Show threads",
"Share room": "Share room",
"Room settings": "Room settings",
"Trusted": "Trusted",
@@ -1927,6 +1926,7 @@
"React": "React",
"Edit": "Edit",
"Reply": "Reply",
+ "Thread": "Thread",
"Message Actions": "Message Actions",
"Download %(text)s": "Download %(text)s",
"Error decrypting attachment": "Error decrypting attachment",
@@ -2413,8 +2413,8 @@
"Clear cache and resync": "Clear cache and resync",
"%(brand)s now uses 3-5x less memory, by only loading information about other users when needed. Please wait whilst we resynchronise with the server!": "%(brand)s now uses 3-5x less memory, by only loading information about other users when needed. Please wait whilst we resynchronise with the server!",
"Updating %(brand)s": "Updating %(brand)s",
- "Leave all rooms and spaces": "Leave all rooms and spaces",
"Don't leave any": "Don't leave any",
+ "Leave all rooms and spaces": "Leave all rooms and spaces",
"Leave specific rooms and spaces": "Leave specific rooms and spaces",
"Search %(spaceName)s": "Search %(spaceName)s",
"You won't be able to rejoin unless you are re-invited.": "You won't be able to rejoin unless you are re-invited.",
@@ -3004,8 +3004,6 @@
"Commands": "Commands",
"Command Autocomplete": "Command Autocomplete",
"Community Autocomplete": "Community Autocomplete",
- "Results from DuckDuckGo": "Results from DuckDuckGo",
- "DuckDuckGo Results": "DuckDuckGo Results",
"Emoji": "Emoji",
"Emoji Autocomplete": "Emoji Autocomplete",
"Notify the whole room": "Notify the whole room",
diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx
index 28c5b1353f..a0261e9994 100644
--- a/src/settings/Settings.tsx
+++ b/src/settings/Settings.tsx
@@ -211,6 +211,15 @@ export const SETTINGS: {[setting: string]: ISetting} = {
supportedLevels: LEVELS_FEATURE,
default: false,
},
+ "feature_thread": {
+ isFeature: true,
+ // Requires a reload as we change an option flag on the `js-sdk`
+ // And the entire sync history needs to be parsed again
+ controller: new ReloadOnChangeController(),
+ displayName: _td("Threaded messaging"),
+ supportedLevels: LEVELS_FEATURE,
+ default: false,
+ },
"feature_custom_status": {
isFeature: true,
displayName: _td("Custom user status messages"),
diff --git a/src/settings/handlers/DeviceSettingsHandler.ts b/src/settings/handlers/DeviceSettingsHandler.ts
index e4ad4cb51e..e57862a824 100644
--- a/src/settings/handlers/DeviceSettingsHandler.ts
+++ b/src/settings/handlers/DeviceSettingsHandler.ts
@@ -71,7 +71,13 @@ export default class DeviceSettingsHandler extends SettingsHandler {
// Special case for old useIRCLayout setting
if (settingName === "layout") {
const settings = this.getSettings() || {};
- if (settings["useIRCLayout"]) return Layout.IRC;
+ if (settings["useIRCLayout"]) {
+ // Set the new layout setting and delete the old one so that we
+ // can delete this block of code after some time
+ settings["layout"] = Layout.IRC;
+ delete settings["useIRCLayout"];
+ localStorage.setItem("mx_local_settings", JSON.stringify(settings));
+ }
return settings[settingName];
}
diff --git a/src/stores/RightPanelStorePhases.ts b/src/stores/RightPanelStorePhases.ts
index d62f6c6110..96a585b676 100644
--- a/src/stores/RightPanelStorePhases.ts
+++ b/src/stores/RightPanelStorePhases.ts
@@ -37,6 +37,10 @@ export enum RightPanelPhases {
SpaceMemberList = "SpaceMemberList",
SpaceMemberInfo = "SpaceMemberInfo",
Space3pidMemberInfo = "Space3pidMemberInfo",
+
+ // Thread stuff
+ ThreadView = "ThreadView",
+ ThreadPanel = "ThreadPanel",
}
// These are the phases that are safe to persist (the ones that don't require additional
diff --git a/src/stores/SpaceStore.tsx b/src/stores/SpaceStore.tsx
index c08c66714b..8a7d51b60a 100644
--- a/src/stores/SpaceStore.tsx
+++ b/src/stores/SpaceStore.tsx
@@ -145,9 +145,9 @@ export class SpaceStoreClass extends AsyncStoreWithClient {
return this._allRoomsInHome;
}
- public async setActiveRoomInSpace(space: Room | null): Promise {
+ public setActiveRoomInSpace(space: Room | null): void {
if (space && !space.isSpaceRoom()) return;
- if (space !== this.activeSpace) await this.setActiveSpace(space);
+ if (space !== this.activeSpace) this.setActiveSpace(space);
if (space) {
const roomId = this.getNotificationState(space.roomId).getFirstRoomWithNotifications();
@@ -190,7 +190,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient {
* @param contextSwitch whether to switch the user's context,
* should not be done when the space switch is done implicitly due to another event like switching room.
*/
- public async setActiveSpace(space: Room | null, contextSwitch = true) {
+ public setActiveSpace(space: Room | null, contextSwitch = true) {
if (space === this.activeSpace || (space && !space.isSpaceRoom())) return;
this._activeSpace = space;
@@ -293,11 +293,15 @@ export class SpaceStoreClass extends AsyncStoreWithClient {
}
if (space) {
- const suggestedRooms = await this.fetchSuggestedRooms(space);
- if (this._activeSpace === space) {
- this._suggestedRooms = suggestedRooms;
- this.emit(SUGGESTED_ROOMS, this._suggestedRooms);
- }
+ this.loadSuggestedRooms(space);
+ }
+ }
+
+ private async loadSuggestedRooms(space) {
+ const suggestedRooms = await this.fetchSuggestedRooms(space);
+ if (this._activeSpace === space) {
+ this._suggestedRooms = suggestedRooms;
+ this.emit(SUGGESTED_ROOMS, this._suggestedRooms);
}
}
@@ -666,6 +670,14 @@ export class SpaceStoreClass extends AsyncStoreWithClient {
this.onSpaceUpdate();
this.emit(room.roomId);
}
+
+ if (room === this.activeSpace && // current space
+ this.matrixClient.getRoom(ev.getStateKey())?.getMyMembership() !== "join" && // target not joined
+ ev.getPrevContent().suggested !== ev.getContent().suggested // suggested flag changed
+ ) {
+ this.loadSuggestedRooms(room);
+ }
+
break;
case EventType.SpaceParent:
@@ -678,12 +690,14 @@ export class SpaceStoreClass extends AsyncStoreWithClient {
}
this.emit(room.roomId);
break;
+ }
+ };
- case EventType.RoomMember:
- if (room.isSpaceRoom()) {
- this.onSpaceMembersChange(ev);
- }
- break;
+ // listening for m.room.member events in onRoomState above doesn't work as the Member object isn't updated by then
+ private onRoomStateMembers = (ev: MatrixEvent) => {
+ const room = this.matrixClient.getRoom(ev.getRoomId());
+ if (room?.isSpaceRoom()) {
+ this.onSpaceMembersChange(ev);
}
};
@@ -743,6 +757,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient {
this.matrixClient.removeListener("Room.myMembership", this.onRoom);
this.matrixClient.removeListener("Room.accountData", this.onRoomAccountData);
this.matrixClient.removeListener("RoomState.events", this.onRoomState);
+ this.matrixClient.removeListener("RoomState.members", this.onRoomStateMembers);
this.matrixClient.removeListener("accountData", this.onAccountData);
}
await this.reset();
@@ -754,6 +769,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient {
this.matrixClient.on("Room.myMembership", this.onRoom);
this.matrixClient.on("Room.accountData", this.onRoomAccountData);
this.matrixClient.on("RoomState.events", this.onRoomState);
+ this.matrixClient.on("RoomState.members", this.onRoomStateMembers);
this.matrixClient.on("accountData", this.onAccountData);
this.matrixClient.getCapabilities().then(capabilities => {
diff --git a/src/stores/widgets/StopGapWidgetDriver.ts b/src/stores/widgets/StopGapWidgetDriver.ts
index 30bea42346..91a4cf6642 100644
--- a/src/stores/widgets/StopGapWidgetDriver.ts
+++ b/src/stores/widgets/StopGapWidgetDriver.ts
@@ -185,7 +185,7 @@ export class StopGapWidgetDriver extends WidgetDriver {
limitPerRoom: number,
roomIds: (string | Symbols.AnyRoom)[] = null,
): Promise