Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Show knock rooms in the list #11573

Merged
merged 12 commits into from
Sep 19, 2023
6 changes: 6 additions & 0 deletions res/css/views/rooms/_NotificationBadge.pcss
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,12 @@ limitations under the License.
border-radius: 6px;
}

&.mx_NotificationBadge_knocked {
mask-image: url("$(res)/img/element-icons/ask-to-join.svg");
width: 12px;
height: 16px;
}

&.mx_NotificationBadge_2char {
width: $font-16px;
height: $font-16px;
Expand Down
2 changes: 1 addition & 1 deletion res/css/views/rooms/_RoomTile.pcss
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ limitations under the License.
mask-image: url("$(res)/img/element-icons/context-menu.svg");
}

&:not(.mx_RoomTile_minimized) {
&:not(.mx_RoomTile_minimized, .mx_RoomTile_sticky) {
&:hover,
&:focus-within,
&.mx_RoomTile_hasMenuOpen {
Expand Down
6 changes: 5 additions & 1 deletion src/RoomNotifs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import type { IPushRule, Room, MatrixClient } from "matrix-js-sdk/src/matrix";
import { NotificationColor } from "./stores/notifications/NotificationColor";
import { getUnsentMessages } from "./components/structures/RoomStatusBar";
import { doesRoomHaveUnreadMessages, doesRoomOrThreadHaveUnreadMessages } from "./Unread";
import { EffectiveMembership, getEffectiveMembership } from "./utils/membership";
import { EffectiveMembership, getEffectiveMembership, isKnockDenied } from "./utils/membership";
import SettingsStore from "./settings/SettingsStore";

export enum RoomNotifState {
Expand Down Expand Up @@ -240,6 +240,10 @@ export function determineUnreadState(
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) {
return { symbol: null, count: 0, color: NotificationColor.None };
}
Expand Down
3 changes: 2 additions & 1 deletion src/components/views/rooms/NotificationBadge.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ export default class NotificationBadge extends React.PureComponent<XOR<IProps, I
/* eslint @typescript-eslint/no-unused-vars: ["error", { "ignoreRestSiblings": true }] */
const { notification, showUnsentTooltip, forceCount, onClick, tabIndex } = this.props;

if (notification.isIdle) return null;
if (notification.isIdle && !notification.knocked) return null;
if (forceCount) {
if (!notification.hasUnreadCount) return null; // Can't render a badge
}
Expand All @@ -131,6 +131,7 @@ export default class NotificationBadge extends React.PureComponent<XOR<IProps, I
symbol: notification.symbol,
count: notification.count,
color: notification.color,
knocked: notification.knocked,
onMouseOver: this.onMouseOver,
onMouseLeave: this.onMouseLeave,
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ interface Props {
symbol: string | null;
count: number;
color: NotificationColor;
knocked?: boolean;
onMouseOver?: (ev: MouseEvent) => void;
onMouseLeave?: (ev: MouseEvent) => void;
children?: ReactNode;
Expand All @@ -45,12 +46,13 @@ export function StatelessNotificationBadge({
symbol,
count,
color,
knocked,
...props
}: XOR<Props, ClickableProps>): JSX.Element {
const hideBold = useSettingValue("feature_hidebold");

// 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 <></>;
}

Expand All @@ -64,9 +66,10 @@ export function StatelessNotificationBadge({

const classes = classNames({
mx_NotificationBadge: true,
mx_NotificationBadge_visible: isEmptyBadge ? true : hasUnreadCount,
mx_NotificationBadge_visible: isEmptyBadge || knocked ? true : hasUnreadCount,
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_3char: symbol && symbol.length > 2,
});
Expand Down
12 changes: 11 additions & 1 deletion src/components/views/rooms/RoomTile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ import { useHasRoomLiveVoiceBroadcast } from "../../../voice-broadcast";
import { RoomTileSubtitle } from "./RoomTileSubtitle";
import { shouldShowComponent } from "../../../customisations/helpers/UIComponents";
import { UIComponent } from "../../../settings/UIFeature";
import { isKnockDenied } from "../../../utils/membership";
import SettingsStore from "../../../settings/SettingsStore";

interface Props {
room: Room;
Expand Down Expand Up @@ -120,7 +122,12 @@ export class RoomTile extends React.PureComponent<ClassProps, State> {
};

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 {
Expand Down Expand Up @@ -378,6 +385,9 @@ export class RoomTile extends React.PureComponent<ClassProps, State> {
public render(): React.ReactElement {
const classes = classNames({
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_hasMenuOpen: !!(this.state.generalMenuPosition || this.state.notificationsMenuPosition),
mx_RoomTile_minimized: this.props.isMinimized,
Expand Down
24 changes: 22 additions & 2 deletions src/stores/notifications/NotificationState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export interface INotificationStateSnapshotParams {
count: number;
color: NotificationColor;
muted: boolean;
knocked: boolean;
}

export enum NotificationStateEvents {
Expand All @@ -44,6 +45,7 @@ export abstract class NotificationState
protected _count = 0;
protected _color: NotificationColor = NotificationColor.None;
protected _muted = false;
protected _knocked = false;

private watcherReferences: string[] = [];

Expand Down Expand Up @@ -72,6 +74,10 @@ export abstract class NotificationState
return this._muted;
}

public get knocked(): boolean {
return this._knocked;
}

public get isIdle(): boolean {
return this.color <= NotificationColor.None;
}
Expand Down Expand Up @@ -117,17 +123,31 @@ export class NotificationStateSnapshot {
private readonly count: number;
private readonly color: NotificationColor;
private readonly muted: boolean;
private readonly knocked: boolean;

public constructor(state: INotificationStateSnapshotParams) {
this.symbol = state.symbol;
this.count = state.count;
this.color = state.color;
this.muted = state.muted;
this.knocked = state.knocked;
}

public isDifferentFrom(other: INotificationStateSnapshotParams): boolean {
const before = { count: this.count, symbol: this.symbol, color: this.color, muted: this.muted };
const after = { count: other.count, symbol: other.symbol, color: other.color, muted: other.muted };
const before = {
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);
}
}
3 changes: 3 additions & 0 deletions src/stores/notifications/RoomNotificationState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { MatrixClientPeg } from "../../MatrixClientPeg";
import { readReceiptChangeIsFor } from "../../utils/read-receipts";
import * as RoomNotifs from "../../RoomNotifs";
import { NotificationState } from "./NotificationState";
import SettingsStore from "../../settings/SettingsStore";

export class RoomNotificationState extends NotificationState implements IDestroyable {
public constructor(public readonly room: Room) {
Expand Down Expand Up @@ -92,10 +93,12 @@ export class RoomNotificationState extends NotificationState implements IDestroy
const { color, symbol, count } = RoomNotifs.determineUnreadState(this.room);
const muted =
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._symbol = symbol;
this._count = count;
this._muted = muted;
this._knocked = knocked;

// finally, publish an update if needed
this.emitIfUpdated(snapshot);
Expand Down
4 changes: 2 additions & 2 deletions src/stores/room-list/RoomListStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import defaultDispatcher, { MatrixDispatcher } from "../../dispatcher/dispatcher
import { readReceiptChangeIsFor } from "../../utils/read-receipts";
import { FILTER_CHANGED, IFilterCondition } from "./filters/IFilterCondition";
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 { MarkedExecution } from "../../utils/MarkedExecution";
import { AsyncStoreWithClient } from "../AsyncStoreWithClient";
Expand Down Expand Up @@ -308,7 +308,7 @@ export class RoomListStoreClass extends AsyncStoreWithClient<IState> implements
public async onDispatchMyMembership(membershipPayload: any): Promise<void> {
// TODO: Type out the dispatcher types so membershipPayload is not any
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 we're joining an upgraded room, we'll want to make sure we don't proliferate
// the dead room in the list.
Expand Down
12 changes: 9 additions & 3 deletions src/stores/room-list/algorithms/Algorithm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,12 @@ import {
ListAlgorithm,
SortAlgorithm,
} from "./models";
import { EffectiveMembership, getEffectiveMembership, splitRoomsByMembership } from "../../../utils/membership";
import {
EffectiveMembership,
getEffectiveMembership,
getEffectiveMembershipTag,
splitRoomsByMembership,
} from "../../../utils/membership";
import { OrderingAlgorithm } from "./list-ordering/OrderingAlgorithm";
import { getListAlgorithmInstance } from "./list-ordering";
import { VisibilityProvider } from "../filters/VisibilityProvider";
Expand Down Expand Up @@ -543,8 +548,9 @@ export class Algorithm extends EventEmitter {
public getTagsForRoom(room: Room): TagID[] {
const tags: TagID[] = [];

const membership = getEffectiveMembership(room.getMyMembership());
if (!membership) return []; // peeked room has no tags
if (!getEffectiveMembership(room.getMyMembership())) return []; // peeked room has no tags

const membership = getEffectiveMembershipTag(room);

if (membership === EffectiveMembership.Invite) {
tags.push(DefaultTagID.Invite);
Expand Down
22 changes: 19 additions & 3 deletions src/utils/membership.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ limitations under the License.

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.
*/
Expand Down Expand Up @@ -55,7 +58,7 @@ export function splitRoomsByMembership(rooms: Room[]): MembershipSplit {
const membership = room.getMyMembership();
// Filter out falsey relationship as this will be peeked rooms
if (!!membership) {
split[getEffectiveMembership(membership)].push(room);
split[getEffectiveMembershipTag(room)].push(room);
}
}

Expand All @@ -65,15 +68,28 @@ export function splitRoomsByMembership(rooms: Room[]): MembershipSplit {
export function getEffectiveMembership(membership: string): EffectiveMembership {
if (membership === "invite") {
return EffectiveMembership.Invite;
} else if (membership === "join") {
// TODO: Include knocks? Update docs as needed in the enum. https://github.com/vector-im/element-web/issues/14237
} else if (membership === "join" || (SettingsStore.getValue("feature_ask_to_join") && membership === "knock")) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment below can be removed now 🥳

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks, removed

return EffectiveMembership.Join;
} else {
// Probably a leave, kick, or ban
return EffectiveMembership.Leave;
}
}

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 {
const effective = getEffectiveMembership(membership);
return effective === EffectiveMembership.Join || effective === EffectiveMembership.Invite;
Expand Down
18 changes: 17 additions & 1 deletion test/RoomNotifs-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import {
} 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 {
getRoomNotifsState,
RoomNotifState,
Expand All @@ -36,6 +36,7 @@ import {
} from "../src/RoomNotifs";
import { NotificationColor } from "../src/stores/notifications/NotificationColor";
import SettingsStore from "../src/settings/SettingsStore";
import { MatrixClientPeg } from "../src/MatrixClientPeg";

describe("RoomNotifs test", () => {
let client: jest.Mocked<MatrixClient>;
Expand Down Expand Up @@ -285,6 +286,21 @@ describe("RoomNotifs test", () => {
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 () => {
room.setUnreadNotificationCount(NotificationCountType.Highlight, 99);
room.setUnreadNotificationCount(NotificationCountType.Total, 99);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,12 @@ describe("StatelessNotificationBadge", () => {
);
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();
});
});
Loading