Show knock rooms in the list (#11573)
* Show knock rooms in the list Signed-off-by: Mikhail Aheichyk <mikhail.aheichyk@nordeck.net> * Pass userId to IndexedDBStore Signed-off-by: Mikhail Aheichyk <mikhail.aheichyk@nordeck.net> * Revert "Pass userId to IndexedDBStore" Signed-off-by: Mikhail Aheichyk <mikhail.aheichyk@nordeck.net> * Code review changes Signed-off-by: Mikhail Aheichyk <mikhail.aheichyk@nordeck.net> --------- Signed-off-by: Mikhail Aheichyk <mikhail.aheichyk@nordeck.net> Co-authored-by: Mikhail Aheichyk <mikhail.aheichyk@nordeck.net>
This commit is contained in:
parent
f9f2e79fd9
commit
86e86ba49f
16 changed files with 187 additions and 24 deletions
|
@ -48,6 +48,12 @@ limitations under the License.
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.mx_NotificationBadge_knocked {
|
||||||
|
mask-image: url("$(res)/img/element-icons/ask-to-join.svg");
|
||||||
|
width: 12px;
|
||||||
|
height: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
&.mx_NotificationBadge_2char {
|
&.mx_NotificationBadge_2char {
|
||||||
width: $font-16px;
|
width: $font-16px;
|
||||||
height: $font-16px;
|
height: $font-16px;
|
||||||
|
|
|
@ -144,7 +144,7 @@ limitations under the License.
|
||||||
mask-image: url("$(res)/img/element-icons/context-menu.svg");
|
mask-image: url("$(res)/img/element-icons/context-menu.svg");
|
||||||
}
|
}
|
||||||
|
|
||||||
&:not(.mx_RoomTile_minimized) {
|
&:not(.mx_RoomTile_minimized, .mx_RoomTile_sticky) {
|
||||||
&:hover,
|
&:hover,
|
||||||
&:focus-within,
|
&:focus-within,
|
||||||
&.mx_RoomTile_hasMenuOpen {
|
&.mx_RoomTile_hasMenuOpen {
|
||||||
|
|
|
@ -27,7 +27,7 @@ import type { IPushRule, Room, MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||||
import { NotificationColor } from "./stores/notifications/NotificationColor";
|
import { NotificationColor } from "./stores/notifications/NotificationColor";
|
||||||
import { getUnsentMessages } from "./components/structures/RoomStatusBar";
|
import { getUnsentMessages } from "./components/structures/RoomStatusBar";
|
||||||
import { doesRoomHaveUnreadMessages, doesRoomOrThreadHaveUnreadMessages } from "./Unread";
|
import { doesRoomHaveUnreadMessages, doesRoomOrThreadHaveUnreadMessages } from "./Unread";
|
||||||
import { EffectiveMembership, getEffectiveMembership } from "./utils/membership";
|
import { EffectiveMembership, getEffectiveMembership, isKnockDenied } from "./utils/membership";
|
||||||
import SettingsStore from "./settings/SettingsStore";
|
import SettingsStore from "./settings/SettingsStore";
|
||||||
|
|
||||||
export enum RoomNotifState {
|
export enum RoomNotifState {
|
||||||
|
@ -240,6 +240,10 @@ export function determineUnreadState(
|
||||||
return { symbol: "!", count: 1, color: NotificationColor.Red };
|
return { symbol: "!", count: 1, color: NotificationColor.Red };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (SettingsStore.getValue("feature_ask_to_join") && isKnockDenied(room)) {
|
||||||
|
return { symbol: "!", count: 1, color: NotificationColor.Red };
|
||||||
|
}
|
||||||
|
|
||||||
if (getRoomNotifsState(room.client, room.roomId) === RoomNotifState.Mute) {
|
if (getRoomNotifsState(room.client, room.roomId) === RoomNotifState.Mute) {
|
||||||
return { symbol: null, count: 0, color: NotificationColor.None };
|
return { symbol: null, count: 0, color: NotificationColor.None };
|
||||||
}
|
}
|
||||||
|
|
|
@ -114,7 +114,7 @@ export default class NotificationBadge extends React.PureComponent<XOR<IProps, I
|
||||||
/* eslint @typescript-eslint/no-unused-vars: ["error", { "ignoreRestSiblings": true }] */
|
/* eslint @typescript-eslint/no-unused-vars: ["error", { "ignoreRestSiblings": true }] */
|
||||||
const { notification, showUnsentTooltip, forceCount, onClick, tabIndex } = this.props;
|
const { notification, showUnsentTooltip, forceCount, onClick, tabIndex } = this.props;
|
||||||
|
|
||||||
if (notification.isIdle) return null;
|
if (notification.isIdle && !notification.knocked) return null;
|
||||||
if (forceCount) {
|
if (forceCount) {
|
||||||
if (!notification.hasUnreadCount) return null; // Can't render a badge
|
if (!notification.hasUnreadCount) return null; // Can't render a badge
|
||||||
}
|
}
|
||||||
|
@ -131,6 +131,7 @@ export default class NotificationBadge extends React.PureComponent<XOR<IProps, I
|
||||||
symbol: notification.symbol,
|
symbol: notification.symbol,
|
||||||
count: notification.count,
|
count: notification.count,
|
||||||
color: notification.color,
|
color: notification.color,
|
||||||
|
knocked: notification.knocked,
|
||||||
onMouseOver: this.onMouseOver,
|
onMouseOver: this.onMouseOver,
|
||||||
onMouseLeave: this.onMouseLeave,
|
onMouseLeave: this.onMouseLeave,
|
||||||
};
|
};
|
||||||
|
|
|
@ -27,6 +27,7 @@ interface Props {
|
||||||
symbol: string | null;
|
symbol: string | null;
|
||||||
count: number;
|
count: number;
|
||||||
color: NotificationColor;
|
color: NotificationColor;
|
||||||
|
knocked?: boolean;
|
||||||
onMouseOver?: (ev: MouseEvent) => void;
|
onMouseOver?: (ev: MouseEvent) => void;
|
||||||
onMouseLeave?: (ev: MouseEvent) => void;
|
onMouseLeave?: (ev: MouseEvent) => void;
|
||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
|
@ -45,12 +46,13 @@ export function StatelessNotificationBadge({
|
||||||
symbol,
|
symbol,
|
||||||
count,
|
count,
|
||||||
color,
|
color,
|
||||||
|
knocked,
|
||||||
...props
|
...props
|
||||||
}: XOR<Props, ClickableProps>): JSX.Element {
|
}: XOR<Props, ClickableProps>): JSX.Element {
|
||||||
const hideBold = useSettingValue("feature_hidebold");
|
const hideBold = useSettingValue("feature_hidebold");
|
||||||
|
|
||||||
// Don't show a badge if we don't need to
|
// Don't show a badge if we don't need to
|
||||||
if (color === NotificationColor.None || (hideBold && color == NotificationColor.Bold)) {
|
if ((color === NotificationColor.None || (hideBold && color == NotificationColor.Bold)) && !knocked) {
|
||||||
return <></>;
|
return <></>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -64,9 +66,10 @@ export function StatelessNotificationBadge({
|
||||||
|
|
||||||
const classes = classNames({
|
const classes = classNames({
|
||||||
mx_NotificationBadge: true,
|
mx_NotificationBadge: true,
|
||||||
mx_NotificationBadge_visible: isEmptyBadge ? true : hasUnreadCount,
|
mx_NotificationBadge_visible: isEmptyBadge || knocked ? true : hasUnreadCount,
|
||||||
mx_NotificationBadge_highlighted: color >= NotificationColor.Red,
|
mx_NotificationBadge_highlighted: color >= NotificationColor.Red,
|
||||||
mx_NotificationBadge_dot: isEmptyBadge,
|
mx_NotificationBadge_dot: isEmptyBadge && !knocked,
|
||||||
|
mx_NotificationBadge_knocked: knocked,
|
||||||
mx_NotificationBadge_2char: symbol && symbol.length > 0 && symbol.length < 3,
|
mx_NotificationBadge_2char: symbol && symbol.length > 0 && symbol.length < 3,
|
||||||
mx_NotificationBadge_3char: symbol && symbol.length > 2,
|
mx_NotificationBadge_3char: symbol && symbol.length > 2,
|
||||||
});
|
});
|
||||||
|
|
|
@ -51,6 +51,8 @@ import { useHasRoomLiveVoiceBroadcast } from "../../../voice-broadcast";
|
||||||
import { RoomTileSubtitle } from "./RoomTileSubtitle";
|
import { RoomTileSubtitle } from "./RoomTileSubtitle";
|
||||||
import { shouldShowComponent } from "../../../customisations/helpers/UIComponents";
|
import { shouldShowComponent } from "../../../customisations/helpers/UIComponents";
|
||||||
import { UIComponent } from "../../../settings/UIFeature";
|
import { UIComponent } from "../../../settings/UIFeature";
|
||||||
|
import { isKnockDenied } from "../../../utils/membership";
|
||||||
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
room: Room;
|
room: Room;
|
||||||
|
@ -120,7 +122,12 @@ export class RoomTile extends React.PureComponent<ClassProps, State> {
|
||||||
};
|
};
|
||||||
|
|
||||||
private get showContextMenu(): boolean {
|
private get showContextMenu(): boolean {
|
||||||
return this.props.tag !== DefaultTagID.Invite && shouldShowComponent(UIComponent.RoomOptionsMenu);
|
return (
|
||||||
|
this.props.tag !== DefaultTagID.Invite &&
|
||||||
|
this.props.room.getMyMembership() !== "knock" &&
|
||||||
|
!isKnockDenied(this.props.room) &&
|
||||||
|
shouldShowComponent(UIComponent.RoomOptionsMenu)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private get showMessagePreview(): boolean {
|
private get showMessagePreview(): boolean {
|
||||||
|
@ -378,6 +385,9 @@ export class RoomTile extends React.PureComponent<ClassProps, State> {
|
||||||
public render(): React.ReactElement {
|
public render(): React.ReactElement {
|
||||||
const classes = classNames({
|
const classes = classNames({
|
||||||
mx_RoomTile: true,
|
mx_RoomTile: true,
|
||||||
|
mx_RoomTile_sticky:
|
||||||
|
SettingsStore.getValue("feature_ask_to_join") &&
|
||||||
|
(this.props.room.getMyMembership() === "knock" || isKnockDenied(this.props.room)),
|
||||||
mx_RoomTile_selected: this.state.selected,
|
mx_RoomTile_selected: this.state.selected,
|
||||||
mx_RoomTile_hasMenuOpen: !!(this.state.generalMenuPosition || this.state.notificationsMenuPosition),
|
mx_RoomTile_hasMenuOpen: !!(this.state.generalMenuPosition || this.state.notificationsMenuPosition),
|
||||||
mx_RoomTile_minimized: this.props.isMinimized,
|
mx_RoomTile_minimized: this.props.isMinimized,
|
||||||
|
|
|
@ -25,6 +25,7 @@ export interface INotificationStateSnapshotParams {
|
||||||
count: number;
|
count: number;
|
||||||
color: NotificationColor;
|
color: NotificationColor;
|
||||||
muted: boolean;
|
muted: boolean;
|
||||||
|
knocked: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum NotificationStateEvents {
|
export enum NotificationStateEvents {
|
||||||
|
@ -44,6 +45,7 @@ export abstract class NotificationState
|
||||||
protected _count = 0;
|
protected _count = 0;
|
||||||
protected _color: NotificationColor = NotificationColor.None;
|
protected _color: NotificationColor = NotificationColor.None;
|
||||||
protected _muted = false;
|
protected _muted = false;
|
||||||
|
protected _knocked = false;
|
||||||
|
|
||||||
private watcherReferences: string[] = [];
|
private watcherReferences: string[] = [];
|
||||||
|
|
||||||
|
@ -72,6 +74,10 @@ export abstract class NotificationState
|
||||||
return this._muted;
|
return this._muted;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public get knocked(): boolean {
|
||||||
|
return this._knocked;
|
||||||
|
}
|
||||||
|
|
||||||
public get isIdle(): boolean {
|
public get isIdle(): boolean {
|
||||||
return this.color <= NotificationColor.None;
|
return this.color <= NotificationColor.None;
|
||||||
}
|
}
|
||||||
|
@ -117,17 +123,31 @@ export class NotificationStateSnapshot {
|
||||||
private readonly count: number;
|
private readonly count: number;
|
||||||
private readonly color: NotificationColor;
|
private readonly color: NotificationColor;
|
||||||
private readonly muted: boolean;
|
private readonly muted: boolean;
|
||||||
|
private readonly knocked: boolean;
|
||||||
|
|
||||||
public constructor(state: INotificationStateSnapshotParams) {
|
public constructor(state: INotificationStateSnapshotParams) {
|
||||||
this.symbol = state.symbol;
|
this.symbol = state.symbol;
|
||||||
this.count = state.count;
|
this.count = state.count;
|
||||||
this.color = state.color;
|
this.color = state.color;
|
||||||
this.muted = state.muted;
|
this.muted = state.muted;
|
||||||
|
this.knocked = state.knocked;
|
||||||
}
|
}
|
||||||
|
|
||||||
public isDifferentFrom(other: INotificationStateSnapshotParams): boolean {
|
public isDifferentFrom(other: INotificationStateSnapshotParams): boolean {
|
||||||
const before = { count: this.count, symbol: this.symbol, color: this.color, muted: this.muted };
|
const before = {
|
||||||
const after = { count: other.count, symbol: other.symbol, color: other.color, muted: other.muted };
|
count: this.count,
|
||||||
|
symbol: this.symbol,
|
||||||
|
color: this.color,
|
||||||
|
muted: this.muted,
|
||||||
|
knocked: this.knocked,
|
||||||
|
};
|
||||||
|
const after = {
|
||||||
|
count: other.count,
|
||||||
|
symbol: other.symbol,
|
||||||
|
color: other.color,
|
||||||
|
muted: other.muted,
|
||||||
|
knocked: other.knocked,
|
||||||
|
};
|
||||||
return JSON.stringify(before) !== JSON.stringify(after);
|
return JSON.stringify(before) !== JSON.stringify(after);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,6 +22,7 @@ import { MatrixClientPeg } from "../../MatrixClientPeg";
|
||||||
import { readReceiptChangeIsFor } from "../../utils/read-receipts";
|
import { readReceiptChangeIsFor } from "../../utils/read-receipts";
|
||||||
import * as RoomNotifs from "../../RoomNotifs";
|
import * as RoomNotifs from "../../RoomNotifs";
|
||||||
import { NotificationState } from "./NotificationState";
|
import { NotificationState } from "./NotificationState";
|
||||||
|
import SettingsStore from "../../settings/SettingsStore";
|
||||||
|
|
||||||
export class RoomNotificationState extends NotificationState implements IDestroyable {
|
export class RoomNotificationState extends NotificationState implements IDestroyable {
|
||||||
public constructor(public readonly room: Room) {
|
public constructor(public readonly room: Room) {
|
||||||
|
@ -92,10 +93,12 @@ export class RoomNotificationState extends NotificationState implements IDestroy
|
||||||
const { color, symbol, count } = RoomNotifs.determineUnreadState(this.room);
|
const { color, symbol, count } = RoomNotifs.determineUnreadState(this.room);
|
||||||
const muted =
|
const muted =
|
||||||
RoomNotifs.getRoomNotifsState(this.room.client, this.room.roomId) === RoomNotifs.RoomNotifState.Mute;
|
RoomNotifs.getRoomNotifsState(this.room.client, this.room.roomId) === RoomNotifs.RoomNotifState.Mute;
|
||||||
|
const knocked = SettingsStore.getValue("feature_ask_to_join") && this.room.getMyMembership() === "knock";
|
||||||
this._color = color;
|
this._color = color;
|
||||||
this._symbol = symbol;
|
this._symbol = symbol;
|
||||||
this._count = count;
|
this._count = count;
|
||||||
this._muted = muted;
|
this._muted = muted;
|
||||||
|
this._knocked = knocked;
|
||||||
|
|
||||||
// finally, publish an update if needed
|
// finally, publish an update if needed
|
||||||
this.emitIfUpdated(snapshot);
|
this.emitIfUpdated(snapshot);
|
||||||
|
|
|
@ -25,7 +25,7 @@ import defaultDispatcher, { MatrixDispatcher } 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, IFilterCondition } from "./filters/IFilterCondition";
|
||||||
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, getEffectiveMembershipTag } from "../../utils/membership";
|
||||||
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";
|
||||||
|
@ -308,7 +308,7 @@ export class RoomListStoreClass extends AsyncStoreWithClient<IState> implements
|
||||||
public async onDispatchMyMembership(membershipPayload: any): Promise<void> {
|
public async onDispatchMyMembership(membershipPayload: any): Promise<void> {
|
||||||
// TODO: Type out the dispatcher types so membershipPayload is not any
|
// TODO: Type out the dispatcher types so membershipPayload is not any
|
||||||
const oldMembership = getEffectiveMembership(membershipPayload.oldMembership);
|
const oldMembership = getEffectiveMembership(membershipPayload.oldMembership);
|
||||||
const newMembership = getEffectiveMembership(membershipPayload.membership);
|
const newMembership = getEffectiveMembershipTag(membershipPayload.room, membershipPayload.membership);
|
||||||
if (oldMembership !== EffectiveMembership.Join && newMembership === EffectiveMembership.Join) {
|
if (oldMembership !== EffectiveMembership.Join && newMembership === EffectiveMembership.Join) {
|
||||||
// If we're joining an upgraded room, we'll want to make sure we don't proliferate
|
// If we're joining an upgraded room, we'll want to make sure we don't proliferate
|
||||||
// the dead room in the list.
|
// the dead room in the list.
|
||||||
|
|
|
@ -30,7 +30,12 @@ import {
|
||||||
ListAlgorithm,
|
ListAlgorithm,
|
||||||
SortAlgorithm,
|
SortAlgorithm,
|
||||||
} from "./models";
|
} from "./models";
|
||||||
import { EffectiveMembership, getEffectiveMembership, splitRoomsByMembership } from "../../../utils/membership";
|
import {
|
||||||
|
EffectiveMembership,
|
||||||
|
getEffectiveMembership,
|
||||||
|
getEffectiveMembershipTag,
|
||||||
|
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";
|
||||||
import { VisibilityProvider } from "../filters/VisibilityProvider";
|
import { VisibilityProvider } from "../filters/VisibilityProvider";
|
||||||
|
@ -543,8 +548,9 @@ export class Algorithm extends EventEmitter {
|
||||||
public getTagsForRoom(room: Room): TagID[] {
|
public getTagsForRoom(room: Room): TagID[] {
|
||||||
const tags: TagID[] = [];
|
const tags: TagID[] = [];
|
||||||
|
|
||||||
const membership = getEffectiveMembership(room.getMyMembership());
|
if (!getEffectiveMembership(room.getMyMembership())) return []; // peeked room has no tags
|
||||||
if (!membership) return []; // peeked room has no tags
|
|
||||||
|
const membership = getEffectiveMembershipTag(room);
|
||||||
|
|
||||||
if (membership === EffectiveMembership.Invite) {
|
if (membership === EffectiveMembership.Invite) {
|
||||||
tags.push(DefaultTagID.Invite);
|
tags.push(DefaultTagID.Invite);
|
||||||
|
|
|
@ -16,6 +16,9 @@ limitations under the License.
|
||||||
|
|
||||||
import { Room, RoomMember, RoomState, RoomStateEvent, MatrixEvent, MatrixClient } from "matrix-js-sdk/src/matrix";
|
import { Room, RoomMember, RoomState, RoomStateEvent, MatrixEvent, MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||||
|
|
||||||
|
import { MatrixClientPeg } from "../MatrixClientPeg";
|
||||||
|
import SettingsStore from "../settings/SettingsStore";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Approximation of a membership status for a given room.
|
* Approximation of a membership status for a given room.
|
||||||
*/
|
*/
|
||||||
|
@ -55,7 +58,7 @@ export function splitRoomsByMembership(rooms: Room[]): MembershipSplit {
|
||||||
const membership = room.getMyMembership();
|
const membership = room.getMyMembership();
|
||||||
// Filter out falsey relationship as this will be peeked rooms
|
// Filter out falsey relationship as this will be peeked rooms
|
||||||
if (!!membership) {
|
if (!!membership) {
|
||||||
split[getEffectiveMembership(membership)].push(room);
|
split[getEffectiveMembershipTag(room)].push(room);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -65,8 +68,7 @@ export function splitRoomsByMembership(rooms: Room[]): MembershipSplit {
|
||||||
export function getEffectiveMembership(membership: string): EffectiveMembership {
|
export function getEffectiveMembership(membership: string): EffectiveMembership {
|
||||||
if (membership === "invite") {
|
if (membership === "invite") {
|
||||||
return EffectiveMembership.Invite;
|
return EffectiveMembership.Invite;
|
||||||
} else if (membership === "join") {
|
} else if (membership === "join" || (SettingsStore.getValue("feature_ask_to_join") && membership === "knock")) {
|
||||||
// TODO: Include knocks? Update docs as needed in the enum. https://github.com/vector-im/element-web/issues/14237
|
|
||||||
return EffectiveMembership.Join;
|
return EffectiveMembership.Join;
|
||||||
} else {
|
} else {
|
||||||
// Probably a leave, kick, or ban
|
// Probably a leave, kick, or ban
|
||||||
|
@ -74,6 +76,20 @@ export function getEffectiveMembership(membership: string): EffectiveMembership
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isKnockDenied(room: Room): boolean | undefined {
|
||||||
|
const memberId = MatrixClientPeg.get()?.getSafeUserId();
|
||||||
|
const member = memberId ? room.getMember(memberId) : null;
|
||||||
|
const previousMembership = member?.events.member?.getPrevContent().membership;
|
||||||
|
|
||||||
|
return member?.isKicked() && previousMembership === "knock";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getEffectiveMembershipTag(room: Room, membership?: string): EffectiveMembership {
|
||||||
|
return isKnockDenied(room)
|
||||||
|
? EffectiveMembership.Join
|
||||||
|
: getEffectiveMembership(membership ?? room.getMyMembership());
|
||||||
|
}
|
||||||
|
|
||||||
export function isJoinedOrNearlyJoined(membership: string): boolean {
|
export function isJoinedOrNearlyJoined(membership: string): boolean {
|
||||||
const effective = getEffectiveMembership(membership);
|
const effective = getEffectiveMembership(membership);
|
||||||
return effective === EffectiveMembership.Join || effective === EffectiveMembership.Invite;
|
return effective === EffectiveMembership.Join || effective === EffectiveMembership.Invite;
|
||||||
|
|
|
@ -27,7 +27,7 @@ import {
|
||||||
} from "matrix-js-sdk/src/matrix";
|
} from "matrix-js-sdk/src/matrix";
|
||||||
|
|
||||||
import type { MatrixClient } from "matrix-js-sdk/src/matrix";
|
import type { MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||||
import { mkEvent, mkRoom, muteRoom, stubClient, upsertRoomStateEvents } from "./test-utils";
|
import { mkEvent, mkRoom, mkRoomMember, muteRoom, stubClient, upsertRoomStateEvents } from "./test-utils";
|
||||||
import {
|
import {
|
||||||
getRoomNotifsState,
|
getRoomNotifsState,
|
||||||
RoomNotifState,
|
RoomNotifState,
|
||||||
|
@ -36,6 +36,7 @@ import {
|
||||||
} from "../src/RoomNotifs";
|
} from "../src/RoomNotifs";
|
||||||
import { NotificationColor } from "../src/stores/notifications/NotificationColor";
|
import { NotificationColor } from "../src/stores/notifications/NotificationColor";
|
||||||
import SettingsStore from "../src/settings/SettingsStore";
|
import SettingsStore from "../src/settings/SettingsStore";
|
||||||
|
import { MatrixClientPeg } from "../src/MatrixClientPeg";
|
||||||
|
|
||||||
describe("RoomNotifs test", () => {
|
describe("RoomNotifs test", () => {
|
||||||
let client: jest.Mocked<MatrixClient>;
|
let client: jest.Mocked<MatrixClient>;
|
||||||
|
@ -285,6 +286,21 @@ describe("RoomNotifs test", () => {
|
||||||
expect(count).toBeGreaterThan(0);
|
expect(count).toBeGreaterThan(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("indicates the user knock has been denied", async () => {
|
||||||
|
jest.spyOn(SettingsStore, "getValue").mockImplementation((name) => {
|
||||||
|
return name === "feature_ask_to_join";
|
||||||
|
});
|
||||||
|
const roomMember = mkRoomMember(room.roomId, MatrixClientPeg.get()!.getSafeUserId(), "leave", true, {
|
||||||
|
membership: "knock",
|
||||||
|
});
|
||||||
|
jest.spyOn(room, "getMember").mockReturnValue(roomMember);
|
||||||
|
const { color, symbol, count } = determineUnreadState(room);
|
||||||
|
|
||||||
|
expect(symbol).toBe("!");
|
||||||
|
expect(color).toBe(NotificationColor.Red);
|
||||||
|
expect(count).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
|
||||||
it("shows nothing for muted channels", async () => {
|
it("shows nothing for muted channels", async () => {
|
||||||
room.setUnreadNotificationCount(NotificationCountType.Highlight, 99);
|
room.setUnreadNotificationCount(NotificationCountType.Highlight, 99);
|
||||||
room.setUnreadNotificationCount(NotificationCountType.Total, 99);
|
room.setUnreadNotificationCount(NotificationCountType.Total, 99);
|
||||||
|
|
|
@ -27,4 +27,12 @@ describe("StatelessNotificationBadge", () => {
|
||||||
);
|
);
|
||||||
expect(container.querySelector(".mx_NotificationBadge_highlighted")).not.toBe(null);
|
expect(container.querySelector(".mx_NotificationBadge_highlighted")).not.toBe(null);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("has knock style", () => {
|
||||||
|
const { container } = render(
|
||||||
|
<StatelessNotificationBadge symbol="!" count={0} color={NotificationColor.Red} knocked={true} />,
|
||||||
|
);
|
||||||
|
expect(container.querySelector(".mx_NotificationBadge_dot")).not.toBeInTheDocument();
|
||||||
|
expect(container.querySelector(".mx_NotificationBadge_knocked")).toBeInTheDocument();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -54,6 +54,8 @@ import { SDKContext } from "../../../../src/contexts/SDKContext";
|
||||||
import { shouldShowComponent } from "../../../../src/customisations/helpers/UIComponents";
|
import { shouldShowComponent } from "../../../../src/customisations/helpers/UIComponents";
|
||||||
import { UIComponent } from "../../../../src/settings/UIFeature";
|
import { UIComponent } from "../../../../src/settings/UIFeature";
|
||||||
import { MessagePreviewStore } from "../../../../src/stores/room-list/MessagePreviewStore";
|
import { MessagePreviewStore } from "../../../../src/stores/room-list/MessagePreviewStore";
|
||||||
|
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
|
||||||
|
import SettingsStore from "../../../../src/settings/SettingsStore";
|
||||||
|
|
||||||
jest.mock("../../../../src/customisations/helpers/UIComponents", () => ({
|
jest.mock("../../../../src/customisations/helpers/UIComponents", () => ({
|
||||||
shouldShowComponent: jest.fn(),
|
shouldShowComponent: jest.fn(),
|
||||||
|
@ -160,8 +162,9 @@ describe("RoomTile", () => {
|
||||||
describe("when message previews are not enabled", () => {
|
describe("when message previews are not enabled", () => {
|
||||||
it("should render the room", () => {
|
it("should render the room", () => {
|
||||||
mocked(shouldShowComponent).mockReturnValue(true);
|
mocked(shouldShowComponent).mockReturnValue(true);
|
||||||
const renderResult = renderRoomTile();
|
const { container } = renderRoomTile();
|
||||||
expect(renderResult.container).toMatchSnapshot();
|
expect(container).toMatchSnapshot();
|
||||||
|
expect(container.querySelector(".mx_RoomTile_sticky")).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("does not render the room options context menu when UIComponent customisations disable room options", () => {
|
it("does not render the room options context menu when UIComponent customisations disable room options", () => {
|
||||||
|
@ -178,6 +181,31 @@ describe("RoomTile", () => {
|
||||||
expect(screen.queryByRole("button", { name: "Room options" })).toBeInTheDocument();
|
expect(screen.queryByRole("button", { name: "Room options" })).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("does not render the room options context menu when knocked to the room", () => {
|
||||||
|
jest.spyOn(SettingsStore, "getValue").mockImplementation((name) => {
|
||||||
|
return name === "feature_ask_to_join";
|
||||||
|
});
|
||||||
|
mocked(shouldShowComponent).mockReturnValue(true);
|
||||||
|
jest.spyOn(room, "getMyMembership").mockReturnValue("knock");
|
||||||
|
const { container } = renderRoomTile();
|
||||||
|
expect(container.querySelector(".mx_RoomTile_sticky")).toBeInTheDocument();
|
||||||
|
expect(screen.queryByRole("button", { name: "Room options" })).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not render the room options context menu when knock has been denied", () => {
|
||||||
|
jest.spyOn(SettingsStore, "getValue").mockImplementation((name) => {
|
||||||
|
return name === "feature_ask_to_join";
|
||||||
|
});
|
||||||
|
mocked(shouldShowComponent).mockReturnValue(true);
|
||||||
|
const roomMember = mkRoomMember(room.roomId, MatrixClientPeg.get()!.getSafeUserId(), "leave", true, {
|
||||||
|
membership: "knock",
|
||||||
|
});
|
||||||
|
jest.spyOn(room, "getMember").mockReturnValue(roomMember);
|
||||||
|
const { container } = renderRoomTile();
|
||||||
|
expect(container.querySelector(".mx_RoomTile_sticky")).toBeInTheDocument();
|
||||||
|
expect(screen.queryByRole("button", { name: "Room options" })).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
describe("when a call starts", () => {
|
describe("when a call starts", () => {
|
||||||
let call: MockedCall;
|
let call: MockedCall;
|
||||||
let widget: Widget;
|
let widget: Widget;
|
||||||
|
|
|
@ -464,14 +464,26 @@ export function mkMembership(
|
||||||
return e;
|
return e;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function mkRoomMember(roomId: string, userId: string, membership = "join"): RoomMember {
|
export function mkRoomMember(
|
||||||
|
roomId: string,
|
||||||
|
userId: string,
|
||||||
|
membership = "join",
|
||||||
|
isKicked = false,
|
||||||
|
prevMemberContent: Partial<IContent> = {},
|
||||||
|
): RoomMember {
|
||||||
return {
|
return {
|
||||||
userId,
|
userId,
|
||||||
membership,
|
membership,
|
||||||
name: userId,
|
name: userId,
|
||||||
rawDisplayName: userId,
|
rawDisplayName: userId,
|
||||||
roomId,
|
roomId,
|
||||||
events: {},
|
events: {
|
||||||
|
member: {
|
||||||
|
getSender: () => undefined,
|
||||||
|
getPrevContent: () => prevMemberContent,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
isKicked: () => isKicked,
|
||||||
getAvatarUrl: () => {},
|
getAvatarUrl: () => {},
|
||||||
getMxcAvatarUrl: () => {},
|
getMxcAvatarUrl: () => {},
|
||||||
getDMInviter: () => {},
|
getDMInviter: () => {},
|
||||||
|
@ -597,6 +609,8 @@ export function mkStubRoom(
|
||||||
roomId: roomId,
|
roomId: roomId,
|
||||||
getAvatarUrl: () => "mxc://avatar.url/image.png",
|
getAvatarUrl: () => "mxc://avatar.url/image.png",
|
||||||
getMxcAvatarUrl: () => "mxc://avatar.url/image.png",
|
getMxcAvatarUrl: () => "mxc://avatar.url/image.png",
|
||||||
|
events: {},
|
||||||
|
isKicked: () => false,
|
||||||
}),
|
}),
|
||||||
getMembers: jest.fn().mockReturnValue([]),
|
getMembers: jest.fn().mockReturnValue([]),
|
||||||
getMembersWithMembership: jest.fn().mockReturnValue([]),
|
getMembersWithMembership: jest.fn().mockReturnValue([]),
|
||||||
|
|
|
@ -17,8 +17,36 @@ limitations under the License.
|
||||||
import { MatrixClient, MatrixEvent, Room, RoomMember, RoomState, RoomStateEvent } from "matrix-js-sdk/src/matrix";
|
import { MatrixClient, MatrixEvent, Room, RoomMember, RoomState, RoomStateEvent } from "matrix-js-sdk/src/matrix";
|
||||||
import { mocked } from "jest-mock";
|
import { mocked } from "jest-mock";
|
||||||
|
|
||||||
import { waitForMember } from "../../src/utils/membership";
|
import { isKnockDenied, waitForMember } from "../../src/utils/membership";
|
||||||
import { createTestClient } from "../test-utils";
|
import { createTestClient, mkRoomMember, stubClient } from "../test-utils";
|
||||||
|
|
||||||
|
describe("isKnockDenied", () => {
|
||||||
|
const userId = "alice";
|
||||||
|
let client: jest.Mocked<MatrixClient>;
|
||||||
|
let room: Room;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
client = stubClient() as jest.Mocked<MatrixClient>;
|
||||||
|
room = new Room("!room-id:example.com", client, "@user:example.com");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("checks that the user knock has been denied", () => {
|
||||||
|
const roomMember = mkRoomMember(room.roomId, userId, "leave", true, { membership: "knock" });
|
||||||
|
jest.spyOn(room, "getMember").mockReturnValue(roomMember);
|
||||||
|
expect(isKnockDenied(room)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
{ membership: "leave", isKicked: false, prevMembership: "invite" },
|
||||||
|
{ membership: "leave", isKicked: true, prevMembership: "invite" },
|
||||||
|
{ membership: "leave", isKicked: false, prevMembership: "join" },
|
||||||
|
{ membership: "leave", isKicked: true, prevMembership: "join" },
|
||||||
|
])("checks that the user knock has been not denied", ({ membership, isKicked, prevMembership }) => {
|
||||||
|
const roomMember = mkRoomMember(room.roomId, userId, membership, isKicked, { membership: prevMembership });
|
||||||
|
jest.spyOn(room, "getMember").mockReturnValue(roomMember);
|
||||||
|
expect(isKnockDenied(room)).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
/* Shorter timeout, we've got tests to run */
|
/* Shorter timeout, we've got tests to run */
|
||||||
const timeout = 30;
|
const timeout = 30;
|
||||||
|
|
Loading…
Reference in a new issue