TAC: Release Announcement (#12380)
* WIP * Store the release announcements in the account settings * Update TAC release announcement description * Fix settings content comparison * Add logging in case of failure * Watch settings changes * I add release announcement settings to disable it * Disable release announcement in e2e test * Add release announcement in e2e test * Add tests for ReleaseAnnouncementStore.ts * Update compound-web to `3.3.0` * Update TAC tests * Update Labs tests * Nits * Add test for ReleaseAnnouncement.tsx * Update `@vector-im/compound-web` * Add playwright snapshot * Delete false playwright screenshot * Wait for EW to be displayed after reload * Add screenshot * Clean util file * Renaming and comments fixing * Use second store instead of looking in the store. --------- Co-authored-by: R Midhun Suresh <hi@midhun.dev>
This commit is contained in:
54
src/components/structures/ReleaseAnnouncement.tsx
Normal file
54
src/components/structures/ReleaseAnnouncement.tsx
Normal file
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
*
|
||||
* Copyright 2024 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, { ComponentProps, JSX, PropsWithChildren } from "react";
|
||||
import { ReleaseAnnouncement as ReleaseAnnouncementCompound } from "@vector-im/compound-web";
|
||||
|
||||
import { ReleaseAnnouncementStore, Feature } from "../../stores/ReleaseAnnouncementStore";
|
||||
import { useIsReleaseAnnouncementOpen } from "../../hooks/useIsReleaseAnnouncementOpen";
|
||||
|
||||
interface ReleaseAnnouncementProps
|
||||
extends Omit<ComponentProps<typeof ReleaseAnnouncementCompound>, "open" | "onClick"> {
|
||||
feature: Feature;
|
||||
}
|
||||
|
||||
/**
|
||||
* Display a release announcement component around the children
|
||||
* Wrapper gluing the release announcement compound and the ReleaseAnnouncementStore
|
||||
* @param feature - the feature to announce, should be listed in {@link Feature}
|
||||
* @param children
|
||||
* @param props
|
||||
* @constructor
|
||||
*/
|
||||
export function ReleaseAnnouncement({
|
||||
feature,
|
||||
children,
|
||||
...props
|
||||
}: PropsWithChildren<ReleaseAnnouncementProps>): JSX.Element {
|
||||
const enabled = useIsReleaseAnnouncementOpen(feature);
|
||||
|
||||
return (
|
||||
<ReleaseAnnouncementCompound
|
||||
open={enabled}
|
||||
onClick={() => ReleaseAnnouncementStore.instance.nextReleaseAnnouncement()}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</ReleaseAnnouncementCompound>
|
||||
);
|
||||
}
|
||||
@@ -34,6 +34,8 @@ import { NotificationLevel } from "../../../../stores/notifications/Notification
|
||||
import PosthogTrackers from "../../../../PosthogTrackers";
|
||||
import { getKeyBindingsManager } from "../../../../KeyBindingsManager";
|
||||
import { KeyBindingAction } from "../../../../accessibility/KeyboardShortcuts";
|
||||
import { ReleaseAnnouncement } from "../../../structures/ReleaseAnnouncement";
|
||||
import { useIsReleaseAnnouncementOpen } from "../../../../hooks/useIsReleaseAnnouncementOpen";
|
||||
|
||||
interface ThreadsActivityCentreProps {
|
||||
/**
|
||||
@@ -49,6 +51,7 @@ interface ThreadsActivityCentreProps {
|
||||
export function ThreadsActivityCentre({ displayButtonLabel }: ThreadsActivityCentreProps): JSX.Element {
|
||||
const [open, setOpen] = useState(false);
|
||||
const roomsAndNotifications = useUnreadThreadRooms(open);
|
||||
const isReleaseAnnouncementOpen = useIsReleaseAnnouncementOpen("threadsActivityCentre");
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -65,41 +68,55 @@ export function ThreadsActivityCentre({ displayButtonLabel }: ThreadsActivityCen
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Menu
|
||||
align="end"
|
||||
open={open}
|
||||
onOpenChange={(newOpen) => {
|
||||
// Track only when the Threads Activity Centre is opened
|
||||
if (newOpen) PosthogTrackers.trackInteraction("WebThreadsActivityCentreButton");
|
||||
|
||||
setOpen(newOpen);
|
||||
}}
|
||||
side="right"
|
||||
title={_t("threads_activity_centre|header")}
|
||||
trigger={
|
||||
{isReleaseAnnouncementOpen ? (
|
||||
<ReleaseAnnouncement
|
||||
feature="threadsActivityCentre"
|
||||
header={_t("threads_activity_centre|release_announcement_header")}
|
||||
description={_t("threads_activity_centre|release_announcement_description")}
|
||||
closeLabel={_t("action|ok")}
|
||||
>
|
||||
<ThreadsActivityCentreButton
|
||||
displayLabel={displayButtonLabel}
|
||||
notificationLevel={roomsAndNotifications.greatestNotificationLevel}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{/* Make the content of the pop-up scrollable */}
|
||||
<div className="mx_ThreadsActivityCentre_rows">
|
||||
{roomsAndNotifications.rooms.map(({ room, notificationLevel }) => (
|
||||
<ThreadsActivityCentreRow
|
||||
key={room.roomId}
|
||||
room={room}
|
||||
notificationLevel={notificationLevel}
|
||||
onClick={() => setOpen(false)}
|
||||
</ReleaseAnnouncement>
|
||||
) : (
|
||||
<Menu
|
||||
align="end"
|
||||
open={open}
|
||||
onOpenChange={(newOpen) => {
|
||||
// Track only when the Threads Activity Centre is opened
|
||||
if (newOpen) PosthogTrackers.trackInteraction("WebThreadsActivityCentreButton");
|
||||
|
||||
setOpen(newOpen);
|
||||
}}
|
||||
side="right"
|
||||
title={_t("threads_activity_centre|header")}
|
||||
trigger={
|
||||
<ThreadsActivityCentreButton
|
||||
displayLabel={displayButtonLabel}
|
||||
notificationLevel={roomsAndNotifications.greatestNotificationLevel}
|
||||
/>
|
||||
))}
|
||||
{roomsAndNotifications.rooms.length === 0 && (
|
||||
<div className="mx_ThreadsActivityCentre_emptyCaption">
|
||||
{_t("threads_activity_centre|no_rooms_with_unreads_threads")}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Menu>
|
||||
}
|
||||
>
|
||||
{/* Make the content of the pop-up scrollable */}
|
||||
<div className="mx_ThreadsActivityCentre_rows">
|
||||
{roomsAndNotifications.rooms.map(({ room, notificationLevel }) => (
|
||||
<ThreadsActivityCentreRow
|
||||
key={room.roomId}
|
||||
room={room}
|
||||
notificationLevel={notificationLevel}
|
||||
onClick={() => setOpen(false)}
|
||||
/>
|
||||
))}
|
||||
{roomsAndNotifications.rooms.length === 0 && (
|
||||
<div className="mx_ThreadsActivityCentre_emptyCaption">
|
||||
{_t("threads_activity_centre|no_rooms_with_unreads_threads")}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Menu>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
32
src/hooks/useIsReleaseAnnouncementOpen.ts
Normal file
32
src/hooks/useIsReleaseAnnouncementOpen.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
*
|
||||
* Copyright 2024 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 { useTypedEventEmitterState } from "./useEventEmitter";
|
||||
import { Feature, ReleaseAnnouncementStore } from "../stores/ReleaseAnnouncementStore";
|
||||
|
||||
/**
|
||||
* Return true if the release announcement of the given feature is enabled
|
||||
* @param feature
|
||||
*/
|
||||
export function useIsReleaseAnnouncementOpen(feature: Feature): boolean {
|
||||
return useTypedEventEmitterState(
|
||||
ReleaseAnnouncementStore.instance,
|
||||
"releaseAnnouncementChanged",
|
||||
() => ReleaseAnnouncementStore.instance.getReleaseAnnouncement() === feature,
|
||||
);
|
||||
}
|
||||
@@ -1417,6 +1417,7 @@
|
||||
"group_spaces": "Spaces",
|
||||
"group_themes": "Themes",
|
||||
"group_threads": "Threads",
|
||||
"group_ui": "User interface",
|
||||
"group_voip": "Voice & Video",
|
||||
"group_widgets": "Widgets",
|
||||
"hidebold": "Hide notification dot (only display counters badges)",
|
||||
@@ -1440,6 +1441,7 @@
|
||||
"oidc_native_flow": "OIDC native authentication",
|
||||
"oidc_native_flow_description": "⚠ WARNING: Experimental. Use OIDC native authentication when supported by the server.",
|
||||
"pinning": "Message Pinning",
|
||||
"release_announcement": "Release announcement",
|
||||
"render_reaction_images": "Render custom images in reactions",
|
||||
"render_reaction_images_description": "Sometimes referred to as \"custom emojis\".",
|
||||
"report_to_moderators": "Report to moderators",
|
||||
@@ -3161,7 +3163,9 @@
|
||||
},
|
||||
"threads_activity_centre": {
|
||||
"header": "Threads activity",
|
||||
"no_rooms_with_unreads_threads": "You don't have rooms with unread threads yet."
|
||||
"no_rooms_with_unreads_threads": "You don't have rooms with unread threads yet.",
|
||||
"release_announcement_description": "Threads notifications have moved, find them here from now on.",
|
||||
"release_announcement_header": "Threads Activity Centre"
|
||||
},
|
||||
"time": {
|
||||
"about_day_ago": "about a day ago",
|
||||
|
||||
@@ -89,6 +89,7 @@ export enum LabGroup {
|
||||
Encryption,
|
||||
Experimental,
|
||||
Developer,
|
||||
Ui,
|
||||
}
|
||||
|
||||
export enum Features {
|
||||
@@ -98,6 +99,7 @@ export enum Features {
|
||||
OidcNativeFlow = "feature_oidc_native_flow",
|
||||
// If true, every new login will use the new rust crypto implementation
|
||||
RustCrypto = "feature_rust_crypto",
|
||||
ReleaseAnnouncement = "feature_release_announcement",
|
||||
}
|
||||
|
||||
export const labGroupNames: Record<LabGroup, TranslationKey> = {
|
||||
@@ -114,6 +116,7 @@ export const labGroupNames: Record<LabGroup, TranslationKey> = {
|
||||
[LabGroup.Encryption]: _td("labs|group_encryption"),
|
||||
[LabGroup.Experimental]: _td("labs|group_experimental"),
|
||||
[LabGroup.Developer]: _td("labs|group_developer"),
|
||||
[LabGroup.Ui]: _td("labs|group_ui"),
|
||||
};
|
||||
|
||||
export type SettingValueType =
|
||||
@@ -1145,6 +1148,24 @@ export const SETTINGS: { [setting: string]: ISetting } = {
|
||||
default: false,
|
||||
isFeature: true,
|
||||
},
|
||||
/**
|
||||
* Enable or disable the release announcement feature
|
||||
*/
|
||||
[Features.ReleaseAnnouncement]: {
|
||||
isFeature: true,
|
||||
labsGroup: LabGroup.Ui,
|
||||
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG,
|
||||
default: true,
|
||||
displayName: _td("labs|release_announcement"),
|
||||
},
|
||||
/**
|
||||
* Managed by the {@link ReleaseAnnouncementStore}
|
||||
* Store the release announcement data
|
||||
*/
|
||||
"releaseAnnouncementData": {
|
||||
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
|
||||
default: {},
|
||||
},
|
||||
[UIFeature.RoomHistorySettings]: {
|
||||
supportedLevels: LEVELS_UI_FEATURE,
|
||||
default: true,
|
||||
|
||||
@@ -17,6 +17,7 @@ limitations under the License.
|
||||
|
||||
import { ClientEvent, MatrixClient, MatrixEvent } from "matrix-js-sdk/src/matrix";
|
||||
import { defer } from "matrix-js-sdk/src/utils";
|
||||
import { isEqual } from "lodash";
|
||||
|
||||
import MatrixClientBackedSettingsHandler from "./MatrixClientBackedSettingsHandler";
|
||||
import { objectClone, objectKeyChanges } from "../../utils/objects";
|
||||
@@ -168,7 +169,7 @@ export default class AccountSettingsHandler extends MatrixClientBackedSettingsHa
|
||||
// which race between different lines.
|
||||
const deferred = defer<void>();
|
||||
const handler = (event: MatrixEvent): void => {
|
||||
if (event.getType() !== eventType || event.getContent()[field] !== value) return;
|
||||
if (event.getType() !== eventType || !isEqual(event.getContent()[field], value)) return;
|
||||
this.client.off(ClientEvent.AccountData, handler);
|
||||
deferred.resolve();
|
||||
};
|
||||
|
||||
176
src/stores/ReleaseAnnouncementStore.ts
Normal file
176
src/stores/ReleaseAnnouncementStore.ts
Normal file
@@ -0,0 +1,176 @@
|
||||
/*
|
||||
*
|
||||
* Copyright 2024 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 { TypedEventEmitter } from "matrix-js-sdk/src/matrix";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
import SettingsStore from "../settings/SettingsStore";
|
||||
import { SettingLevel } from "../settings/SettingLevel";
|
||||
import { Features } from "../settings/Settings";
|
||||
|
||||
/**
|
||||
* The features are shown in the array order.
|
||||
*/
|
||||
const FEATURES = ["threadsActivityCentre"] as const;
|
||||
/**
|
||||
* All the features that can be shown in the release announcements.
|
||||
*/
|
||||
export type Feature = (typeof FEATURES)[number];
|
||||
/**
|
||||
* The stored settings for the release announcements.
|
||||
* The boolean is at true when the user has viewed the feature
|
||||
*/
|
||||
type StoredSettings = Record<Feature, boolean>;
|
||||
|
||||
/**
|
||||
* The events emitted by the ReleaseAnnouncementStore.
|
||||
*/
|
||||
type ReleaseAnnouncementStoreEvents = "releaseAnnouncementChanged";
|
||||
/**
|
||||
* The handlers for the ReleaseAnnouncementStore events.
|
||||
*/
|
||||
type HandlerMap = {
|
||||
releaseAnnouncementChanged: (newFeature: Feature | null) => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* The ReleaseAnnouncementStore is responsible for managing the release announcements.
|
||||
* It keeps track of the viewed release announcements and emits events when the release announcement changes.
|
||||
*/
|
||||
export class ReleaseAnnouncementStore extends TypedEventEmitter<ReleaseAnnouncementStoreEvents, HandlerMap> {
|
||||
/**
|
||||
* The singleton instance of the ReleaseAnnouncementStore.
|
||||
* @private
|
||||
*/
|
||||
private static internalInstance: ReleaseAnnouncementStore;
|
||||
/**
|
||||
* The index of the feature to show.
|
||||
* @private
|
||||
*/
|
||||
private index = 0;
|
||||
|
||||
/**
|
||||
* The singleton instance of the ReleaseAnnouncementStore.
|
||||
*/
|
||||
public static get instance(): ReleaseAnnouncementStore {
|
||||
if (!ReleaseAnnouncementStore.internalInstance) {
|
||||
ReleaseAnnouncementStore.internalInstance = new ReleaseAnnouncementStore();
|
||||
}
|
||||
return ReleaseAnnouncementStore.internalInstance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Should be used only for testing purposes.
|
||||
* @internal
|
||||
*/
|
||||
public constructor() {
|
||||
super();
|
||||
SettingsStore.watchSetting("releaseAnnouncementData", null, () => {
|
||||
this.emit("releaseAnnouncementChanged", this.getReleaseAnnouncement());
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the viewed release announcements from the settings.
|
||||
* @private
|
||||
*/
|
||||
private getViewedReleaseAnnouncements(): StoredSettings {
|
||||
return SettingsStore.getValue<StoredSettings>("releaseAnnouncementData");
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the release announcement is enabled.
|
||||
* @private
|
||||
*/
|
||||
private isReleaseAnnouncementEnabled(): boolean {
|
||||
return SettingsStore.getValue<boolean>(Features.ReleaseAnnouncement);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the release announcement that should be displayed
|
||||
* @returns The feature to announce or null if there is no feature to announce
|
||||
*/
|
||||
public getReleaseAnnouncement(): Feature | null {
|
||||
// Do nothing if the release announcement is disabled
|
||||
const isReleaseAnnouncementEnabled = this.isReleaseAnnouncementEnabled();
|
||||
if (!isReleaseAnnouncementEnabled) return null;
|
||||
|
||||
const viewedReleaseAnnouncements = this.getViewedReleaseAnnouncements();
|
||||
|
||||
// Find the first feature that has not been viewed
|
||||
for (let i = this.index; i < FEATURES.length; i++) {
|
||||
if (!viewedReleaseAnnouncements[FEATURES[i]]) {
|
||||
this.index = i;
|
||||
return FEATURES[this.index];
|
||||
}
|
||||
}
|
||||
|
||||
// All features have been viewed
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark the current release announcement as viewed.
|
||||
* This will update the account settings
|
||||
* @private
|
||||
*/
|
||||
private async markReleaseAnnouncementAsViewed(): Promise<void> {
|
||||
// Do nothing if the release announcement is disabled
|
||||
const isReleaseAnnouncementEnabled = this.isReleaseAnnouncementEnabled();
|
||||
if (!isReleaseAnnouncementEnabled) return;
|
||||
|
||||
const viewedReleaseAnnouncements = this.getViewedReleaseAnnouncements();
|
||||
|
||||
// If the index is out of bounds, do nothing
|
||||
// Normally it shouldn't happen, but it's better to be safe
|
||||
const feature = FEATURES[this.index];
|
||||
if (!feature) return;
|
||||
|
||||
// Mark the feature as viewed
|
||||
viewedReleaseAnnouncements[FEATURES[this.index]] = true;
|
||||
this.index++;
|
||||
|
||||
// Do sanity check if we can store the new value in the settings
|
||||
const isSupported = SettingsStore.isLevelSupported(SettingLevel.ACCOUNT);
|
||||
if (!isSupported) return;
|
||||
|
||||
const canSetValue = SettingsStore.canSetValue("releaseAnnouncementData", null, SettingLevel.ACCOUNT);
|
||||
if (canSetValue) {
|
||||
try {
|
||||
await SettingsStore.setValue(
|
||||
"releaseAnnouncementData",
|
||||
null,
|
||||
SettingLevel.ACCOUNT,
|
||||
viewedReleaseAnnouncements,
|
||||
);
|
||||
} catch (e) {
|
||||
logger.log("Failed to set release announcement settings", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark the current release announcement as viewed and move to the next release announcement.
|
||||
* This will update the account settings and emit the `releaseAnnouncementChanged` event
|
||||
*/
|
||||
public async nextReleaseAnnouncement(): Promise<void> {
|
||||
await this.markReleaseAnnouncementAsViewed();
|
||||
|
||||
this.emit("releaseAnnouncementChanged", this.getReleaseAnnouncement());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user