Skip to content
This repository has been archived by the owner on Sep 11, 2024. It is now read-only.

Commit

Permalink
Show knock rooms in the list (#11573)
Browse files Browse the repository at this point in the history
* Show knock rooms in the list

Signed-off-by: Mikhail Aheichyk <[email protected]>

* Pass userId to IndexedDBStore

Signed-off-by: Mikhail Aheichyk <[email protected]>

* Revert "Pass userId to IndexedDBStore"

Signed-off-by: Mikhail Aheichyk <[email protected]>

* Code review changes

Signed-off-by: Mikhail Aheichyk <[email protected]>

---------

Signed-off-by: Mikhail Aheichyk <[email protected]>
Co-authored-by: Mikhail Aheichyk <[email protected]>
  • Loading branch information
maheichyk and Mikhail Aheichyk authored Sep 19, 2023
1 parent f9f2e79 commit 86e86ba
Show file tree
Hide file tree
Showing 16 changed files with 187 additions and 24 deletions.
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")) {
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

0 comments on commit 86e86ba

Please sign in to comment.