🧵 Enable threads by default (#9736)

* Delabs threads

* remove threads reload when labs is toggled

* Fix ts strict

* fix rebase mistake

* remove .only

* fix pr comments

* re-introduce backwards compat

* Fix export test

* Fix SearchREsultTile test

* strict ts
This commit is contained in:
Germain 2022-12-13 15:09:15 +00:00 committed by GitHub
parent 9668a24ca7
commit 2d2755d145
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 88 additions and 170 deletions

View File

@ -77,7 +77,6 @@ describe("Polls", () => {
}; };
beforeEach(() => { beforeEach(() => {
cy.enableLabsFeature("feature_thread");
cy.window().then((win) => { cy.window().then((win) => {
win.localStorage.setItem("mx_lhs_size", "0"); // Collapse left panel for these tests win.localStorage.setItem("mx_lhs_size", "0"); // Collapse left panel for these tests
}); });

View File

@ -19,17 +19,10 @@ limitations under the License.
import { SynapseInstance } from "../../plugins/synapsedocker"; import { SynapseInstance } from "../../plugins/synapsedocker";
import { MatrixClient } from "../../global"; import { MatrixClient } from "../../global";
function markWindowBeforeReload(): void {
// mark our window object to "know" when it gets reloaded
cy.window().then((w) => (w.beforeReload = true));
}
describe("Threads", () => { describe("Threads", () => {
let synapse: SynapseInstance; let synapse: SynapseInstance;
beforeEach(() => { beforeEach(() => {
// Default threads to ON for this spec
cy.enableLabsFeature("feature_thread");
cy.window().then((win) => { cy.window().then((win) => {
win.localStorage.setItem("mx_lhs_size", "0"); // Collapse left panel for these tests win.localStorage.setItem("mx_lhs_size", "0"); // Collapse left panel for these tests
}); });
@ -44,35 +37,6 @@ describe("Threads", () => {
cy.stopSynapse(synapse); cy.stopSynapse(synapse);
}); });
it("should reload when enabling threads beta", () => {
markWindowBeforeReload();
// Turn off
cy.openUserSettings("Labs").within(() => {
// initially the new property is there
cy.window().should("have.prop", "beforeReload", true);
cy.leaveBeta("Threads");
cy.wait(1000);
// after reload the property should be gone
cy.window().should("not.have.prop", "beforeReload");
});
cy.get(".mx_MatrixChat", { timeout: 15000 }); // wait for the app
markWindowBeforeReload();
// Turn on
cy.openUserSettings("Labs").within(() => {
// initially the new property is there
cy.window().should("have.prop", "beforeReload", true);
cy.joinBeta("Threads");
cy.wait(1000);
// after reload the property should be gone
cy.window().should("not.have.prop", "beforeReload");
});
});
it("should be usable for a conversation", () => { it("should be usable for a conversation", () => {
let bot: MatrixClient; let bot: MatrixClient;
cy.getBot(synapse, { cy.getBot(synapse, {

View File

@ -118,13 +118,6 @@ limitations under the License.
color: $primary-content; color: $primary-content;
} }
&.mx_MessageActionBar_threadButton {
.mx_Indicator {
background: $links;
animation-iteration-count: infinite;
}
}
&.mx_MessageActionBar_favouriteButton_fillstar { &.mx_MessageActionBar_favouriteButton_fillstar {
color: var(--MessageActionBar-star-button-color); color: var(--MessageActionBar-star-button-color);
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 85 KiB

View File

@ -233,7 +233,7 @@ class MatrixClientPegClass implements IMatrixClientPeg {
opts.pendingEventOrdering = PendingEventOrdering.Detached; opts.pendingEventOrdering = PendingEventOrdering.Detached;
opts.lazyLoadMembers = true; opts.lazyLoadMembers = true;
opts.clientWellKnownPollPeriod = 2 * 60 * 60; // 2 hours opts.clientWellKnownPollPeriod = 2 * 60 * 60; // 2 hours
opts.experimentalThreadSupport = SettingsStore.getValue("feature_thread"); opts.experimentalThreadSupport = SettingsStore.getValue("feature_threadstable");
if (SettingsStore.getValue("feature_sliding_sync")) { if (SettingsStore.getValue("feature_sliding_sync")) {
const proxyUrl = SettingsStore.getValue("feature_sliding_sync_proxy_url"); const proxyUrl = SettingsStore.getValue("feature_sliding_sync_proxy_url");

View File

@ -287,7 +287,7 @@ export default class MessagePanel extends React.Component<IProps, IState> {
// and we check this in a hot code path. This is also cached in our // and we check this in a hot code path. This is also cached in our
// RoomContext, however we still need a fallback for roomless MessagePanels. // RoomContext, however we still need a fallback for roomless MessagePanels.
this._showHiddenEvents = SettingsStore.getValue("showHiddenEventsInTimeline"); this._showHiddenEvents = SettingsStore.getValue("showHiddenEventsInTimeline");
this.threadsEnabled = SettingsStore.getValue("feature_thread"); this.threadsEnabled = SettingsStore.getValue("feature_threadstable");
this.showTypingNotificationsWatcherRef = SettingsStore.watchSetting( this.showTypingNotificationsWatcherRef = SettingsStore.watchSetting(
"showTypingNotifications", "showTypingNotifications",

View File

@ -33,6 +33,7 @@ import ResizeNotifier from "../../utils/ResizeNotifier";
import MatrixClientContext from "../../contexts/MatrixClientContext"; import MatrixClientContext from "../../contexts/MatrixClientContext";
import { RoomPermalinkCreator } from "../../utils/permalinks/Permalinks"; import { RoomPermalinkCreator } from "../../utils/permalinks/Permalinks";
import RoomContext from "../../contexts/RoomContext"; import RoomContext from "../../contexts/RoomContext";
import SettingsStore from "../../settings/SettingsStore";
const DEBUG = false; const DEBUG = false;
let debuglog = function (msg: string) {}; let debuglog = function (msg: string) {};
@ -98,7 +99,7 @@ export const RoomSearchView = forwardRef<ScrollPanel, Props>(
return b.length - a.length; return b.length - a.length;
}); });
if (client.supportsExperimentalThreads()) { if (SettingsStore.getValue("feature_threadstable")) {
// Process all thread roots returned in this batch of search results // Process all thread roots returned in this batch of search results
// XXX: This won't work for results coming from Seshat which won't include the bundled relationship // XXX: This won't work for results coming from Seshat which won't include the bundled relationship
for (const result of results.results) { for (const result of results.results) {

View File

@ -1177,7 +1177,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
CHAT_EFFECTS.forEach((effect) => { CHAT_EFFECTS.forEach((effect) => {
if (containsEmoji(ev.getContent(), effect.emojis) || ev.getContent().msgtype === effect.msgType) { if (containsEmoji(ev.getContent(), effect.emojis) || ev.getContent().msgtype === effect.msgType) {
// For initial threads launch, chat effects are disabled see #19731 // For initial threads launch, chat effects are disabled see #19731
if (!SettingsStore.getValue("feature_thread") || !ev.isRelation(THREAD_RELATION_TYPE.name)) { if (!SettingsStore.getValue("feature_threadstable") || !ev.isRelation(THREAD_RELATION_TYPE.name)) {
dis.dispatch({ action: `effects.${effect.command}` }); dis.dispatch({ action: `effects.${effect.command}` });
} }
} }

View File

@ -249,7 +249,7 @@ const ThreadPanel: React.FC<IProps> = ({ roomId, onClose, permalinkCreator }) =>
const openFeedback = shouldShowFeedback() const openFeedback = shouldShowFeedback()
? () => { ? () => {
Modal.createDialog(BetaFeedbackDialog, { Modal.createDialog(BetaFeedbackDialog, {
featureId: "feature_thread", featureId: "feature_threadstable",
}); });
} }
: null; : null;

View File

@ -1683,7 +1683,7 @@ class TimelinePanel extends React.Component<IProps, IState> {
is very tied to the main room timeline, we are forcing the timeline to is very tied to the main room timeline, we are forcing the timeline to
send read receipts for threaded events */ send read receipts for threaded events */
const isThreadTimeline = this.context.timelineRenderingType === TimelineRenderingType.Thread; const isThreadTimeline = this.context.timelineRenderingType === TimelineRenderingType.Thread;
if (SettingsStore.getValue("feature_thread") && isThreadTimeline) { if (SettingsStore.getValue("feature_threadstable") && isThreadTimeline) {
return 0; return 0;
} }
const index = this.state.events.findIndex((ev) => ev.getId() === evId); const index = this.state.events.findIndex((ev) => ev.getId() === evId);

View File

@ -71,11 +71,7 @@ const ReplyInThreadButton = ({ mxEvent, closeMenu }: IReplyInThreadButton) => {
if (Boolean(relationType) && relationType !== RelationType.Thread) return null; if (Boolean(relationType) && relationType !== RelationType.Thread) return null;
const onClick = (): void => { const onClick = (): void => {
if (!localStorage.getItem("mx_seen_feature_thread")) { if (!SettingsStore.getValue("feature_threadstable")) {
localStorage.setItem("mx_seen_feature_thread", "true");
}
if (!SettingsStore.getValue("feature_thread")) {
dis.dispatch({ dis.dispatch({
action: Action.ViewUserSettings, action: Action.ViewUserSettings,
initialTabId: UserTab.Labs, initialTabId: UserTab.Labs,
@ -644,7 +640,7 @@ export default class MessageContextMenu extends React.Component<IProps, IState>
rightClick && rightClick &&
contentActionable && contentActionable &&
canSendMessages && canSendMessages &&
SettingsStore.getValue("feature_thread") && SettingsStore.getValue("feature_threadstable") &&
Thread.hasServerSideSupport && Thread.hasServerSideSupport &&
timelineRenderingType !== TimelineRenderingType.Thread timelineRenderingType !== TimelineRenderingType.Thread
) { ) {

View File

@ -56,7 +56,6 @@ import { Key } from "../../../Keyboard";
import { ALTERNATE_KEY_NAME } from "../../../accessibility/KeyboardShortcuts"; import { ALTERNATE_KEY_NAME } from "../../../accessibility/KeyboardShortcuts";
import { UserTab } from "../dialogs/UserTab"; import { UserTab } from "../dialogs/UserTab";
import { Action } from "../../../dispatcher/actions"; import { Action } from "../../../dispatcher/actions";
import SdkConfig from "../../../SdkConfig";
import { ShowThreadPayload } from "../../../dispatcher/payloads/ShowThreadPayload"; import { ShowThreadPayload } from "../../../dispatcher/payloads/ShowThreadPayload";
import useFavouriteMessages from "../../../hooks/useFavouriteMessages"; import useFavouriteMessages from "../../../hooks/useFavouriteMessages";
import { GetRelationsForEvent } from "../rooms/EventTile"; import { GetRelationsForEvent } from "../rooms/EventTile";
@ -204,8 +203,7 @@ const ReplyInThreadButton = ({ mxEvent }: IReplyInThreadButton) => {
const relationType = mxEvent?.getRelation()?.rel_type; const relationType = mxEvent?.getRelation()?.rel_type;
const hasARelation = !!relationType && relationType !== RelationType.Thread; const hasARelation = !!relationType && relationType !== RelationType.Thread;
const firstTimeSeeingThreads = !localStorage.getItem("mx_seen_feature_thread"); const threadsEnabled = SettingsStore.getValue("feature_threadstable");
const threadsEnabled = SettingsStore.getValue("feature_thread");
if (!threadsEnabled && !Thread.hasServerSideSupport) { if (!threadsEnabled && !Thread.hasServerSideSupport) {
// hide the prompt if the user would only have degraded mode // hide the prompt if the user would only have degraded mode
@ -217,11 +215,7 @@ const ReplyInThreadButton = ({ mxEvent }: IReplyInThreadButton) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
if (firstTimeSeeingThreads) { if (!SettingsStore.getValue("feature_threadstable")) {
localStorage.setItem("mx_seen_feature_thread", "true");
}
if (!SettingsStore.getValue("feature_thread")) {
dis.dispatch({ dis.dispatch({
action: Action.ViewUserSettings, action: Action.ViewUserSettings,
initialTabId: UserTab.Labs, initialTabId: UserTab.Labs,
@ -257,7 +251,7 @@ const ReplyInThreadButton = ({ mxEvent }: IReplyInThreadButton) => {
</div> </div>
{!hasARelation && ( {!hasARelation && (
<div className="mx_Tooltip_sub"> <div className="mx_Tooltip_sub">
{SettingsStore.getValue("feature_thread") {SettingsStore.getValue("feature_threadstable")
? _t("Beta feature") ? _t("Beta feature")
: _t("Beta feature. Click to learn more.")} : _t("Beta feature. Click to learn more.")}
</div> </div>
@ -273,7 +267,6 @@ const ReplyInThreadButton = ({ mxEvent }: IReplyInThreadButton) => {
onContextMenu={onClick} onContextMenu={onClick}
> >
<ThreadIcon /> <ThreadIcon />
{firstTimeSeeingThreads && !threadsEnabled && <div className="mx_Indicator" />}
</RovingAccessibleTooltipButton> </RovingAccessibleTooltipButton>
); );
}; };
@ -393,21 +386,6 @@ export default class MessageActionBar extends React.PureComponent<IMessageAction
private readonly forbiddenThreadHeadMsgType = [MsgType.KeyVerificationRequest]; private readonly forbiddenThreadHeadMsgType = [MsgType.KeyVerificationRequest];
private get showReplyInThreadAction(): boolean { private get showReplyInThreadAction(): boolean {
if (!SettingsStore.getValue("feature_thread") && !Thread.hasServerSideSupport) {
// hide the prompt if the user would only have degraded mode
return null;
}
if (
!SettingsStore.getBetaInfo("feature_thread") &&
!SettingsStore.getValue("feature_thread") &&
!SdkConfig.get("show_labs_settings")
) {
// Hide the beta prompt if there is no UI to enable it,
// e.g if config.json disables it and doesn't enable show labs flags
return false;
}
const inNotThreadTimeline = this.context.timelineRenderingType !== TimelineRenderingType.Thread; const inNotThreadTimeline = this.context.timelineRenderingType !== TimelineRenderingType.Thread;
const isAllowedMessageType = const isAllowedMessageType =
@ -568,7 +546,7 @@ export default class MessageActionBar extends React.PureComponent<IMessageAction
); );
} }
} else if ( } else if (
SettingsStore.getValue("feature_thread") && SettingsStore.getValue("feature_threadstable") &&
// Show thread icon even for deleted messages, but only within main timeline // Show thread icon even for deleted messages, but only within main timeline
this.context.timelineRenderingType === TimelineRenderingType.Room && this.context.timelineRenderingType === TimelineRenderingType.Room &&
this.props.mxEvent.getThread() this.props.mxEvent.getThread()

View File

@ -297,7 +297,7 @@ export default class RoomHeaderButtons extends HeaderButtons<IProps> {
); );
rightPanelPhaseButtons.set( rightPanelPhaseButtons.set(
RightPanelPhases.ThreadPanel, RightPanelPhases.ThreadPanel,
SettingsStore.getValue("feature_thread") ? ( SettingsStore.getValue("feature_threadstable") ? (
<HeaderButton <HeaderButton
key={RightPanelPhases.ThreadPanel} key={RightPanelPhases.ThreadPanel}
name="threadsButton" name="threadsButton"

View File

@ -387,7 +387,7 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
} }
} }
if (SettingsStore.getValue("feature_thread")) { if (SettingsStore.getValue("feature_threadstable")) {
this.props.mxEvent.on(ThreadEvent.Update, this.updateThread); this.props.mxEvent.on(ThreadEvent.Update, this.updateThread);
if (this.thread && !this.supportsThreadNotifications) { if (this.thread && !this.supportsThreadNotifications) {
@ -469,7 +469,7 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
if (this.props.showReactions) { if (this.props.showReactions) {
this.props.mxEvent.removeListener(MatrixEventEvent.RelationsCreated, this.onReactionsCreated); this.props.mxEvent.removeListener(MatrixEventEvent.RelationsCreated, this.onReactionsCreated);
} }
if (SettingsStore.getValue("feature_thread")) { if (SettingsStore.getValue("feature_threadstable")) {
this.props.mxEvent.off(ThreadEvent.Update, this.updateThread); this.props.mxEvent.off(ThreadEvent.Update, this.updateThread);
} }
this.threadState?.off(NotificationStateEvents.Update, this.onThreadStateUpdate); this.threadState?.off(NotificationStateEvents.Update, this.onThreadStateUpdate);
@ -496,7 +496,7 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
}; };
private get thread(): Thread | null { private get thread(): Thread | null {
if (!SettingsStore.getValue("feature_thread")) { if (!SettingsStore.getValue("feature_threadstable")) {
return null; return null;
} }

View File

@ -67,7 +67,7 @@ export default class SearchResultTile extends React.Component<IProps> {
const layout = SettingsStore.getValue("layout"); const layout = SettingsStore.getValue("layout");
const isTwelveHour = SettingsStore.getValue("showTwelveHourTimestamps"); const isTwelveHour = SettingsStore.getValue("showTwelveHourTimestamps");
const alwaysShowTimestamps = SettingsStore.getValue("alwaysShowTimestamps"); const alwaysShowTimestamps = SettingsStore.getValue("alwaysShowTimestamps");
const threadsEnabled = SettingsStore.getValue("feature_thread"); const threadsEnabled = SettingsStore.getValue("feature_threadstable");
const timeline = result.context.getTimeline(); const timeline = result.context.getTimeline();
for (let j = 0; j < timeline.length; j++) { for (let j = 0; j < timeline.length; j++) {

View File

@ -436,7 +436,7 @@ export class SendMessageComposer extends React.Component<ISendMessageComposerPro
// For initial threads launch, chat effects are disabled // For initial threads launch, chat effects are disabled
// see #19731 // see #19731
const isNotThread = this.props.relation?.rel_type !== THREAD_RELATION_TYPE.name; const isNotThread = this.props.relation?.rel_type !== THREAD_RELATION_TYPE.name;
if (!SettingsStore.getValue("feature_thread") || isNotThread) { if (!SettingsStore.getValue("feature_threadstable") || isNotThread) {
dis.dispatch({ action: `effects.${effect.command}` }); dis.dispatch({ action: `effects.${effect.command}` });
} }
} }

View File

@ -108,7 +108,7 @@ export function sendMessage(message: string, isHTML: boolean, { roomContext, mxC
// For initial threads launch, chat effects are disabled // For initial threads launch, chat effects are disabled
// see #19731 // see #19731
const isNotThread = relation?.rel_type !== THREAD_RELATION_TYPE.name; const isNotThread = relation?.rel_type !== THREAD_RELATION_TYPE.name;
if (!SettingsStore.getValue("feature_thread") || isNotThread) { if (!SettingsStore.getValue("feature_threadstable") || isNotThread) {
dis.dispatch({ action: `effects.${effect.command}` }); dis.dispatch({ action: `effects.${effect.command}` });
} }
} }

View File

@ -915,15 +915,9 @@
"In rooms that support moderation, the “Report” button will let you report abuse to room moderators.": "In rooms that support moderation, the “Report” button will let you report abuse to room moderators.", "In rooms that support moderation, the “Report” button will let you report abuse to room moderators.": "In rooms that support moderation, the “Report” button will let you report abuse to room moderators.",
"Render LaTeX maths in messages": "Render LaTeX maths in messages", "Render LaTeX maths in messages": "Render LaTeX maths in messages",
"Message Pinning": "Message Pinning", "Message Pinning": "Message Pinning",
"Threaded messaging": "Threaded messaging", "Threaded messages": "Threaded messages",
"Keep discussions organised with threads.": "Keep discussions organised with threads.", "Keep discussions organised with threads.": "Keep discussions organised with threads.",
"Threads help keep conversations on-topic and easy to track. <a>Learn more</a>.": "Threads help keep conversations on-topic and easy to track. <a>Learn more</a>.", "Threads help keep conversations on-topic and easy to track. <a>Learn more</a>.": "Threads help keep conversations on-topic and easy to track. <a>Learn more</a>.",
"How can I start a thread?": "How can I start a thread?",
"Use “%(replyInThread)s” when hovering over a message.": "Use “%(replyInThread)s” when hovering over a message.",
"Reply in thread": "Reply in thread",
"How can I leave the beta?": "How can I leave the beta?",
"To leave, return to this page and use the “%(leaveTheBeta)s” button.": "To leave, return to this page and use the “%(leaveTheBeta)s” button.",
"Leave the beta": "Leave the beta",
"Rich text editor": "Rich text editor", "Rich text editor": "Rich text editor",
"Use rich text instead of Markdown in the message composer. Plain text mode coming soon.": "Use rich text instead of Markdown in the message composer. Plain text mode coming soon.", "Use rich text instead of Markdown in the message composer. Plain text mode coming soon.": "Use rich text instead of Markdown in the message composer. Plain text mode coming soon.",
"Render simple counters in room header": "Render simple counters in room header", "Render simple counters in room header": "Render simple counters in room header",
@ -2322,6 +2316,7 @@
"Error processing audio message": "Error processing audio message", "Error processing audio message": "Error processing audio message",
"View live location": "View live location", "View live location": "View live location",
"React": "React", "React": "React",
"Reply in thread": "Reply in thread",
"Can't create a thread from an event with an existing relation": "Can't create a thread from an event with an existing relation", "Can't create a thread from an event with an existing relation": "Can't create a thread from an event with an existing relation",
"Beta feature": "Beta feature", "Beta feature": "Beta feature",
"Beta feature. Click to learn more.": "Beta feature. Click to learn more.", "Beta feature. Click to learn more.": "Beta feature. Click to learn more.",
@ -3198,6 +3193,7 @@
"Beta": "Beta", "Beta": "Beta",
"Leaving the beta will reload %(brand)s.": "Leaving the beta will reload %(brand)s.", "Leaving the beta will reload %(brand)s.": "Leaving the beta will reload %(brand)s.",
"Joining the beta will reload %(brand)s.": "Joining the beta will reload %(brand)s.", "Joining the beta will reload %(brand)s.": "Joining the beta will reload %(brand)s.",
"Leave the beta": "Leave the beta",
"Join the beta": "Join the beta", "Join the beta": "Join the beta",
"Updated %(humanizedUpdateTime)s": "Updated %(humanizedUpdateTime)s", "Updated %(humanizedUpdateTime)s": "Updated %(humanizedUpdateTime)s",
"Live until %(expiryTime)s": "Live until %(expiryTime)s", "Live until %(expiryTime)s": "Live until %(expiryTime)s",

View File

@ -255,15 +255,15 @@ export const SETTINGS: { [setting: string]: ISetting } = {
supportedLevels: LEVELS_FEATURE, supportedLevels: LEVELS_FEATURE,
default: false, default: false,
}, },
"feature_thread": { "feature_threadstable": {
isFeature: true, isFeature: true,
labsGroup: LabGroup.Messaging, labsGroup: LabGroup.Messaging,
controller: new ThreadBetaController(), controller: new ThreadBetaController(),
displayName: _td("Threaded messaging"), displayName: _td("Threaded messages"),
supportedLevels: LEVELS_FEATURE, supportedLevels: LEVELS_FEATURE,
default: false, default: true,
betaInfo: { betaInfo: {
title: _td("Threads"), title: _td("Threaded messages"),
caption: () => ( caption: () => (
<> <>
<p>{_t("Keep discussions organised with threads.")}</p> <p>{_t("Keep discussions organised with threads.")}</p>
@ -282,28 +282,6 @@ export const SETTINGS: { [setting: string]: ISetting } = {
</p> </p>
</> </>
), ),
faq: () =>
SdkConfig.get().bug_report_endpoint_url && (
<>
<h4>{_t("How can I start a thread?")}</h4>
<p>
{_t("Use “%(replyInThread)s” when hovering over a message.", {
replyInThread: _t("Reply in thread"),
})}
</p>
<h4>{_t("How can I leave the beta?")}</h4>
<p>
{_t("To leave, return to this page and use the “%(leaveTheBeta)s” button.", {
leaveTheBeta: _t("Leave the beta"),
})}
</p>
</>
),
feedbackLabel: "thread-feedback",
feedbackSubheading: _td(
"Thank you for trying the beta, " + "please go into as much detail as you can so we can improve it.",
),
image: require("../../res/img/betas/threads.png"),
requiresRefresh: true, requiresRefresh: true,
}, },
}, },

View File

@ -65,7 +65,7 @@ export default class TypingStore {
if (SettingsStore.getValue("lowBandwidth")) return; if (SettingsStore.getValue("lowBandwidth")) return;
// Disable typing notification for threads for the initial launch // Disable typing notification for threads for the initial launch
// before we figure out a better user experience for them // before we figure out a better user experience for them
if (SettingsStore.getValue("feature_thread") && threadId) return; if (SettingsStore.getValue("feature_threadstable") && threadId) return;
let currentTyping = this.typingStates[roomId]; let currentTyping = this.typingStates[roomId];
if ((!isTyping && !currentTyping) || (currentTyping && currentTyping.isTyping === isTyping)) { if ((!isTyping && !currentTyping) || (currentTyping && currentTyping.isTyping === isTyping)) {

View File

@ -278,10 +278,10 @@ export default class RightPanelStore extends ReadyWatchingStore {
// (A nicer fix could be to indicate, that the right panel is loading if there is missing state data and re-emit if the data is available) // (A nicer fix could be to indicate, that the right panel is loading if there is missing state data and re-emit if the data is available)
switch (card.phase) { switch (card.phase) {
case RightPanelPhases.ThreadPanel: case RightPanelPhases.ThreadPanel:
if (!SettingsStore.getValue("feature_thread")) return false; if (!SettingsStore.getValue("feature_threadstable")) return false;
break; break;
case RightPanelPhases.ThreadView: case RightPanelPhases.ThreadView:
if (!SettingsStore.getValue("feature_thread")) return false; if (!SettingsStore.getValue("feature_threadstable")) return false;
if (!card.state.threadHeadEvent) { if (!card.state.threadHeadEvent) {
logger.warn("removed card from right panel because of missing threadHeadEvent in card state"); logger.warn("removed card from right panel because of missing threadHeadEvent in card state");
} }

View File

@ -236,7 +236,7 @@ export class StopGapWidgetDriver extends WidgetDriver {
// For initial threads launch, chat effects are disabled // For initial threads launch, chat effects are disabled
// see #19731 // see #19731
const isNotThread = content["m.relates_to"].rel_type !== THREAD_RELATION_TYPE.name; const isNotThread = content["m.relates_to"].rel_type !== THREAD_RELATION_TYPE.name;
if (!SettingsStore.getValue("feature_thread") || isNotThread) { if (!SettingsStore.getValue("feature_threadstable") || isNotThread) {
dis.dispatch({ action: `effects.${effect.command}` }); dis.dispatch({ action: `effects.${effect.command}` });
} }
} }

View File

@ -176,7 +176,7 @@ export function makeReplyMixIn(ev?: MatrixEvent): IEventRelation {
}; };
if (ev.threadRootId) { if (ev.threadRootId) {
if (SettingsStore.getValue("feature_thread")) { if (SettingsStore.getValue("feature_threadstable")) {
mixin.is_falling_back = false; mixin.is_falling_back = false;
} else { } else {
// Clients that do not offer a threading UI should behave as follows when replying, for best interaction // Clients that do not offer a threading UI should behave as follows when replying, for best interaction
@ -203,7 +203,7 @@ export function shouldDisplayReply(event: MatrixEvent): boolean {
const relation = event.getRelation(); const relation = event.getRelation();
if ( if (
SettingsStore.getValue("feature_thread") && SettingsStore.getValue("feature_threadstable") &&
relation?.rel_type === THREAD_RELATION_TYPE.name && relation?.rel_type === THREAD_RELATION_TYPE.name &&
relation?.is_falling_back relation?.is_falling_back
) { ) {

View File

@ -62,7 +62,7 @@ export default class HTMLExporter extends Exporter {
this.mediaOmitText = !this.exportOptions.attachmentsIncluded this.mediaOmitText = !this.exportOptions.attachmentsIncluded
? _t("Media omitted") ? _t("Media omitted")
: _t("Media omitted - file size limit exceeded"); : _t("Media omitted - file size limit exceeded");
this.threadsEnabled = SettingsStore.getValue("feature_thread"); this.threadsEnabled = SettingsStore.getValue("feature_threadstable");
} }
protected async getRoomAvatar() { protected async getRoomAvatar() {

View File

@ -169,6 +169,7 @@ describe("TimelinePanel", () => {
const getValueCopy = SettingsStore.getValue; const getValueCopy = SettingsStore.getValue;
SettingsStore.getValue = jest.fn().mockImplementation((name: string) => { SettingsStore.getValue = jest.fn().mockImplementation((name: string) => {
if (name === "sendReadReceipts") return true; if (name === "sendReadReceipts") return true;
if (name === "feature_threadstable") return false;
return getValueCopy(name); return getValueCopy(name);
}); });
@ -182,6 +183,7 @@ describe("TimelinePanel", () => {
const getValueCopy = SettingsStore.getValue; const getValueCopy = SettingsStore.getValue;
SettingsStore.getValue = jest.fn().mockImplementation((name: string) => { SettingsStore.getValue = jest.fn().mockImplementation((name: string) => {
if (name === "sendReadReceipts") return false; if (name === "sendReadReceipts") return false;
if (name === "feature_threadstable") return false;
return getValueCopy(name); return getValueCopy(name);
}); });
@ -358,7 +360,7 @@ describe("TimelinePanel", () => {
client.supportsExperimentalThreads = () => true; client.supportsExperimentalThreads = () => true;
const getValueCopy = SettingsStore.getValue; const getValueCopy = SettingsStore.getValue;
SettingsStore.getValue = jest.fn().mockImplementation((name: string) => { SettingsStore.getValue = jest.fn().mockImplementation((name: string) => {
if (name === "feature_thread") return true; if (name === "feature_threadstable") return true;
return getValueCopy(name); return getValueCopy(name);
}); });

View File

@ -386,6 +386,12 @@ describe("<MessageActionBar />", () => {
}); });
describe("when threads feature is not enabled", () => { describe("when threads feature is not enabled", () => {
beforeEach(() => {
jest.spyOn(SettingsStore, "getValue").mockImplementation(
(setting) => setting !== "feature_threadstable",
);
});
it("does not render thread button when threads does not have server support", () => { it("does not render thread button when threads does not have server support", () => {
jest.spyOn(SettingsStore, "getValue").mockReturnValue(false); jest.spyOn(SettingsStore, "getValue").mockReturnValue(false);
Thread.setServerSideSupport(FeatureSupport.None); Thread.setServerSideSupport(FeatureSupport.None);
@ -416,7 +422,9 @@ describe("<MessageActionBar />", () => {
describe("when threads feature is enabled", () => { describe("when threads feature is enabled", () => {
beforeEach(() => { beforeEach(() => {
jest.spyOn(SettingsStore, "getValue").mockImplementation((setting) => setting === "feature_thread"); jest.spyOn(SettingsStore, "getValue").mockImplementation(
(setting) => setting === "feature_threadstable",
);
}); });
it("renders thread button on own actionable event", () => { it("renders thread button on own actionable event", () => {

View File

@ -40,7 +40,7 @@ describe("RoomHeaderButtons-test.tsx", function () {
}); });
jest.spyOn(SettingsStore, "getValue").mockImplementation((name: string) => { jest.spyOn(SettingsStore, "getValue").mockImplementation((name: string) => {
if (name === "feature_thread") return true; if (name === "feature_threadstable") return true;
}); });
}); });

View File

@ -73,7 +73,7 @@ describe("EventTile", () => {
jest.spyOn(client, "getRoom").mockReturnValue(room); jest.spyOn(client, "getRoom").mockReturnValue(room);
jest.spyOn(client, "decryptEventIfNeeded").mockResolvedValue(); jest.spyOn(client, "decryptEventIfNeeded").mockResolvedValue();
jest.spyOn(SettingsStore, "getValue").mockImplementation((name) => name === "feature_thread"); jest.spyOn(SettingsStore, "getValue").mockImplementation((name) => name === "feature_threadstable");
mxEvent = mkMessage({ mxEvent = mkMessage({
room: room.roomId, room: room.roomId,

View File

@ -19,14 +19,21 @@ import { SearchResult } from "matrix-js-sdk/src/models/search-result";
import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { EventType } from "matrix-js-sdk/src/@types/event"; import { EventType } from "matrix-js-sdk/src/@types/event";
import { render } from "@testing-library/react"; import { render } from "@testing-library/react";
import { Room } from "matrix-js-sdk/src/models/room";
import { createTestClient } from "../../../test-utils"; import { stubClient } from "../../../test-utils";
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
import SearchResultTile from "../../../../src/components/views/rooms/SearchResultTile"; import SearchResultTile from "../../../../src/components/views/rooms/SearchResultTile";
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
const ROOM_ID = "!qPewotXpIctQySfjSy:localhost";
describe("SearchResultTile", () => { describe("SearchResultTile", () => {
beforeAll(() => { beforeAll(() => {
MatrixClientPeg.get = () => createTestClient(); stubClient();
const cli = MatrixClientPeg.get();
const room = new Room(ROOM_ID, cli, "@bob:example.org");
jest.spyOn(cli, "getRoom").mockReturnValue(room);
}); });
it("Sets up appropriate callEventGrouper for m.call. events", () => { it("Sets up appropriate callEventGrouper for m.call. events", () => {
@ -44,7 +51,7 @@ describe("SearchResultTile", () => {
}, },
event_id: "$144429830826TWwbB:localhost", event_id: "$144429830826TWwbB:localhost",
origin_server_ts: 1432735824653, origin_server_ts: 1432735824653,
room_id: "!qPewotXpIctQySfjSy:localhost", room_id: ROOM_ID,
sender: "@example:example.org", sender: "@example:example.org",
type: "m.room.message", type: "m.room.message",
unsigned: { unsigned: {
@ -59,7 +66,7 @@ describe("SearchResultTile", () => {
{ {
type: EventType.CallInvite, type: EventType.CallInvite,
sender: "@user1:server", sender: "@user1:server",
room_id: "!qPewotXpIctQySfjSy:localhost", room_id: ROOM_ID,
origin_server_ts: 1432735824652, origin_server_ts: 1432735824652,
content: { call_id: "call.1" }, content: { call_id: "call.1" },
event_id: "$1:server", event_id: "$1:server",
@ -69,7 +76,7 @@ describe("SearchResultTile", () => {
{ {
type: EventType.CallAnswer, type: EventType.CallAnswer,
sender: "@user2:server", sender: "@user2:server",
room_id: "!qPewotXpIctQySfjSy:localhost", room_id: ROOM_ID,
origin_server_ts: 1432735824654, origin_server_ts: 1432735824654,
content: { call_id: "call.1" }, content: { call_id: "call.1" },
event_id: "$2:server", event_id: "$2:server",

View File

@ -80,7 +80,7 @@ exports[`<SecurityUserSettingsTab /> renders settings marked as beta as beta car
class="mx_BetaCard_title" class="mx_BetaCard_title"
> >
<span> <span>
Threads Threaded messages
</span> </span>
<span <span
class="mx_BetaCard_betaPill" class="mx_BetaCard_betaPill"
@ -124,9 +124,6 @@ exports[`<SecurityUserSettingsTab /> renders settings marked as beta as beta car
> >
Joining the beta will reload . Joining the beta will reload .
</div> </div>
<div
class="mx_BetaCard_faq"
/>
</div> </div>
<div <div
class="mx_BetaCard_columns_image_wrapper" class="mx_BetaCard_columns_image_wrapper"
@ -134,7 +131,6 @@ exports[`<SecurityUserSettingsTab /> renders settings marked as beta as beta car
<img <img
alt="" alt=""
class="mx_BetaCard_columns_image" class="mx_BetaCard_columns_image"
src="image-file-stub"
/> />
</div> </div>
</div> </div>

View File

@ -14,7 +14,6 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { mocked } from "jest-mock";
import { MatrixClient } from "matrix-js-sdk/src/matrix"; import { MatrixClient } from "matrix-js-sdk/src/matrix";
import TypingStore from "../../src/stores/TypingStore"; import TypingStore from "../../src/stores/TypingStore";
@ -31,10 +30,6 @@ jest.mock("../../src/settings/SettingsStore", () => ({
describe("TypingStore", () => { describe("TypingStore", () => {
let typingStore: TypingStore; let typingStore: TypingStore;
let mockClient: MatrixClient; let mockClient: MatrixClient;
const settings = {
sendTypingNotifications: true,
feature_thread: false,
};
const roomId = "!test:example.com"; const roomId = "!test:example.com";
const localRoomId = LOCAL_ROOM_ID_PREFIX + "test"; const localRoomId = LOCAL_ROOM_ID_PREFIX + "test";
@ -45,8 +40,8 @@ describe("TypingStore", () => {
const context = new TestSdkContext(); const context = new TestSdkContext();
context.client = mockClient; context.client = mockClient;
typingStore = new TypingStore(context); typingStore = new TypingStore(context);
mocked(SettingsStore.getValue).mockImplementation((setting: string) => { jest.spyOn(SettingsStore, "getValue").mockImplementation((name: string) => {
return settings[setting]; return name === "sendTypingNotifications";
}); });
}); });

View File

@ -45,25 +45,32 @@ interface ITestContent extends IContent {
} }
describe("export", function () { describe("export", function () {
stubClient(); let mockExportOptions: IExportOptions;
client = MatrixClientPeg.get(); let mockRoom: Room;
client.getUserId = () => { let ts0: number;
return MY_USER_ID; let events: MatrixEvent[];
}; beforeEach(() => {
stubClient();
client = MatrixClientPeg.get();
client.getUserId = () => {
return MY_USER_ID;
};
const mockExportOptions: IExportOptions = { mockExportOptions = {
numberOfMessages: 5, numberOfMessages: 5,
maxSize: 100 * 1024 * 1024, maxSize: 100 * 1024 * 1024,
attachmentsIncluded: false, attachmentsIncluded: false,
}; };
function createRoom() { function createRoom() {
const room = new Room(generateRoomId(), null, client.getUserId()); const room = new Room(generateRoomId(), null, client.getUserId());
return room; return room;
} }
const mockRoom = createRoom(); mockRoom = createRoom();
ts0 = Date.now();
const ts0 = Date.now(); events = mkEvents();
jest.spyOn(client, "getRoom").mockReturnValue(mockRoom);
});
function mkRedactedEvent(i = 0) { function mkRedactedEvent(i = 0) {
return new MatrixEvent({ return new MatrixEvent({
@ -218,8 +225,6 @@ describe("export", function () {
return matrixEvents; return matrixEvents;
} }
const events: MatrixEvent[] = mkEvents();
it("checks if the export format is valid", function () { it("checks if the export format is valid", function () {
function isValidFormat(format: string): boolean { function isValidFormat(format: string): boolean {
const options: string[] = Object.values(ExportFormat); const options: string[] = Object.values(ExportFormat);