From 7a1a2c41d2b1d8fb4e925dd428459e1e397848a1 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Thu, 14 Apr 2022 11:50:55 +0100
Subject: [PATCH] Improve Threads beta around degraded mode (#8318)

* Hide MAB Threads prompt if user would have degraded mode

* Confirm user wants to enable Threads beta if in degraded mode

* fix

* Fix copy
---
 .../views/messages/MessageActionBar.tsx       | 14 +++--
 src/i18n/strings/en_EN.json                   |  4 ++
 src/settings/Settings.tsx                     |  5 +-
 src/settings/SettingsStore.ts                 |  9 ++--
 src/settings/controllers/SettingController.ts | 11 ++++
 .../controllers/ThreadBetaController.tsx      | 53 +++++++++++++++++++
 6 files changed, 85 insertions(+), 11 deletions(-)
 create mode 100644 src/settings/controllers/ThreadBetaController.tsx

diff --git a/src/components/views/messages/MessageActionBar.tsx b/src/components/views/messages/MessageActionBar.tsx
index 80f9b6daf8..7bca48097c 100644
--- a/src/components/views/messages/MessageActionBar.tsx
+++ b/src/components/views/messages/MessageActionBar.tsx
@@ -20,6 +20,7 @@ import React, { ReactElement, useContext, useEffect } from 'react';
 import { EventStatus, MatrixEvent, MatrixEventEvent } from 'matrix-js-sdk/src/models/event';
 import classNames from 'classnames';
 import { MsgType, RelationType } from 'matrix-js-sdk/src/@types/event';
+import { Thread } from 'matrix-js-sdk/src/models/thread';
 
 import type { Relations } from 'matrix-js-sdk/src/models/relations';
 import { _t } from '../../../languageHandler';
@@ -164,11 +165,16 @@ const ReplyInThreadButton = ({ mxEvent }: IReplyInThreadButton) => {
 
     const relationType = mxEvent?.getRelation()?.rel_type;
     const hasARelation = !!relationType && relationType !== RelationType.Thread;
-    const firstTimeSeeingThreads = localStorage.getItem("mx_seen_feature_thread") === null &&
-        !SettingsStore.getValue("feature_thread");
+    const firstTimeSeeingThreads = !localStorage.getItem("mx_seen_feature_thread");
+    const threadsEnabled = SettingsStore.getValue("feature_thread");
+
+    if (!threadsEnabled && !Thread.hasServerSideSupport) {
+        // hide the prompt if the user would only have degraded mode
+        return null;
+    }
 
     const onClick = (): void => {
-        if (localStorage.getItem("mx_seen_feature_thread") === null) {
+        if (firstTimeSeeingThreads) {
             localStorage.setItem("mx_seen_feature_thread", "true");
         }
 
@@ -219,7 +225,7 @@ const ReplyInThreadButton = ({ mxEvent }: IReplyInThreadButton) => {
 
         onClick={onClick}
     >
-        { firstTimeSeeingThreads && (
+        { firstTimeSeeingThreads && !threadsEnabled && (
             <div className="mx_Indicator" />
         ) }
     </RovingAccessibleTooltipButton>;
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index a3ab40d344..bc238477f9 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -960,6 +960,10 @@
     "Automatically send debug logs on any error": "Automatically send debug logs on any error",
     "Automatically send debug logs on decryption errors": "Automatically send debug logs on decryption errors",
     "Automatically send debug logs when key backup is not functioning": "Automatically send debug logs when key backup is not functioning",
+    "Partial Support for Threads": "Partial Support for Threads",
+    "Your homeserver does not currently support threads, so this feature may be unreliable. Some threaded messages may not be reliably available. <a>Learn more</a>.": "Your homeserver does not currently support threads, so this feature may be unreliable. Some threaded messages may not be reliably available. <a>Learn more</a>.",
+    "Do you want to enable threads anyway?": "Do you want to enable threads anyway?",
+    "Yes, enable": "Yes, enable",
     "Collecting app version information": "Collecting app version information",
     "Collecting logs": "Collecting logs",
     "Uploading logs": "Uploading logs",
diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx
index 8cd842b74e..bd34297161 100644
--- a/src/settings/Settings.tsx
+++ b/src/settings/Settings.tsx
@@ -42,6 +42,7 @@ import IncompatibleController from "./controllers/IncompatibleController";
 import { ImageSize } from "./enums/ImageSize";
 import { MetaSpace } from "../stores/spaces";
 import SdkConfig from "../SdkConfig";
+import ThreadBetaController from './controllers/ThreadBetaController';
 
 // These are just a bunch of helper arrays to avoid copy/pasting a bunch of times
 const LEVELS_ROOM_SETTINGS = [
@@ -222,9 +223,7 @@ export const SETTINGS: {[setting: string]: ISetting} = {
     "feature_thread": {
         isFeature: true,
         labsGroup: LabGroup.Messaging,
-        // 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(),
+        controller: new ThreadBetaController(),
         displayName: _td("Threaded messaging"),
         supportedLevels: LEVELS_FEATURE,
         default: false,
diff --git a/src/settings/SettingsStore.ts b/src/settings/SettingsStore.ts
index ca9bfe3703..95ea0e6993 100644
--- a/src/settings/SettingsStore.ts
+++ b/src/settings/SettingsStore.ts
@@ -451,12 +451,13 @@ export default class SettingsStore {
             throw new Error("User cannot set " + settingName + " at " + level + " in " + roomId);
         }
 
+        if (setting.controller && !(await setting.controller.beforeChange(level, roomId, value))) {
+            return; // controller says no
+        }
+
         await handler.setValue(settingName, roomId, value);
 
-        const controller = setting.controller;
-        if (controller) {
-            controller.onChange(level, roomId, value);
-        }
+        setting.controller?.onChange(level, roomId, value);
     }
 
     /**
diff --git a/src/settings/controllers/SettingController.ts b/src/settings/controllers/SettingController.ts
index 292b2d63e5..a274bcff2c 100644
--- a/src/settings/controllers/SettingController.ts
+++ b/src/settings/controllers/SettingController.ts
@@ -46,6 +46,17 @@ export default abstract class SettingController {
         return null; // no override
     }
 
+    /**
+     * Called before the setting value has been changed, can abort the change.
+     * @param {string} level The level at which the setting has been modified.
+     * @param {String} roomId The room ID, may be null.
+     * @param {*} newValue The new value for the setting, may be null.
+     * @return {boolean} Whether the settings change should be accepted.
+     */
+    public async beforeChange(level: SettingLevel, roomId: string, newValue: any): Promise<boolean> {
+        return true;
+    }
+
     /**
      * Called when the setting value has been changed.
      * @param {string} level The level at which the setting has been modified.
diff --git a/src/settings/controllers/ThreadBetaController.tsx b/src/settings/controllers/ThreadBetaController.tsx
new file mode 100644
index 0000000000..487d2013b1
--- /dev/null
+++ b/src/settings/controllers/ThreadBetaController.tsx
@@ -0,0 +1,53 @@
+/*
+Copyright 2022 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import * as React from "react";
+import { Thread } from "matrix-js-sdk/src/models/thread";
+
+import SettingController from "./SettingController";
+import PlatformPeg from "../../PlatformPeg";
+import { SettingLevel } from "../SettingLevel";
+import Modal from "../../Modal";
+import QuestionDialog from "../../components/views/dialogs/QuestionDialog";
+import { _t } from "../../languageHandler";
+
+export default class ThreadBetaController extends SettingController {
+    public async beforeChange(level: SettingLevel, roomId: string, newValue: any): Promise<boolean> {
+        if (Thread.hasServerSideSupport || !newValue) return true; // Full support or user is disabling
+
+        const { finished } = Modal.createTrackedDialog<[boolean]>("Thread beta", "degraded mode", QuestionDialog, {
+            title: _t("Partial Support for Threads"),
+            description: <>
+                <p>{ _t("Your homeserver does not currently support threads, so this feature may be unreliable. " +
+                    "Some threaded messages may not be reliably available. <a>Learn more</a>.", {}, {
+                    a: sub => (
+                        <a href="https://element.io/help#threads" target="_blank" rel="noreferrer noopener">{ sub }</a>
+                    ),
+                }) }</p>
+                <p>{ _t("Do you want to enable threads anyway?") }</p>
+            </>,
+            button: _t("Yes, enable"),
+        });
+        const [enable] = await finished;
+        return enable;
+    }
+
+    public onChange(level: SettingLevel, roomId: string, newValue: any) {
+        // Requires a reload as we change an option flag on the `js-sdk`
+        // And the entire sync history needs to be parsed again
+        PlatformPeg.get().reload();
+    }
+}