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

feat(matrix-client): implement and handle adding / removing custom mute conversation tags #2052

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions src/lib/chat/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ export interface RealtimeChatEvents {
receiveLiveRoomEvent: (eventData) => void;
roomFavorited: (roomId: string) => void;
roomUnfavorited: (roomId: string) => void;
roomMuted: (roomId: string) => void;
roomUnmuted: (roomId: string) => void;
roomMemberTyping: (roomId: string, userIds: string[]) => void;
roomMemberPowerLevelChanged: (roomId: string, matrixId: string, powerLevel: number) => void;
readReceiptReceived: (messageId: string, userId: string) => void;
Expand Down Expand Up @@ -304,6 +306,14 @@ export async function removeRoomFromFavorites(roomId: string) {
return await chat.get().matrix.removeRoomFromFavorites(roomId);
}

export async function addRoomToMuted(roomId: string) {
return await chat.get().matrix.addRoomToMuted(roomId);
}

export async function removeRoomFromMuted(roomId: string) {
return await chat.get().matrix.removeRoomFromMuted(roomId);
}

export async function uploadImageUrl(
channelId: string,
url: string,
Expand Down
70 changes: 70 additions & 0 deletions src/lib/chat/matrix-client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1144,4 +1144,74 @@ describe('matrix client', () => {
expect(isFavorite).toBe(false);
});
});

describe('addRoomToMuted', () => {
it('sets room tag with "m.mute"', async () => {
const roomId = '!testRoomId';
const setRoomTag = jest.fn().mockResolvedValue({});

const client = subject({
createClient: jest.fn(() => getSdkClient({ setRoomTag })),
});

await client.connect(null, 'token');
await client.addRoomToMuted(roomId);

expect(setRoomTag).toHaveBeenCalledWith(roomId, 'm.mute');
});
});

describe('removeRoomFromMuted', () => {
it('deletes "m.mute" tag from room', async () => {
const roomId = '!testRoomId';
const deleteRoomTag = jest.fn().mockResolvedValue({});

const client = subject({
createClient: jest.fn(() => getSdkClient({ deleteRoomTag })),
});

await client.connect(null, 'token');
await client.removeRoomFromMuted(roomId);

expect(deleteRoomTag).toHaveBeenCalledWith(roomId, 'm.mute');
});
});

describe('isRoomMuted', () => {
it('returns true if "m.mute" tag is present for room', async () => {
const roomId = '!testRoomId';
const getRoomTags = jest.fn().mockResolvedValue({
tags: {
'm.mute': {},
},
});

const client = subject({
createClient: jest.fn(() => getSdkClient({ getRoomTags })),
});

await client.connect(null, 'token');
const isMuted = await client.isRoomMuted(roomId);

expect(getRoomTags).toHaveBeenCalledWith(roomId);
expect(isMuted).toBe(true);
});

it('returns false if "m.mute" tag is not present for room', async () => {
const roomId = '!testRoomId';
const getRoomTags = jest.fn().mockResolvedValue({
tags: {},
});

const client = subject({
createClient: jest.fn(() => getSdkClient({ getRoomTags })),
});

await client.connect(null, 'token');
const isMuted = await client.isRoomMuted(roomId);

expect(getRoomTags).toHaveBeenCalledWith(roomId);
expect(isMuted).toBe(false);
});
});
});
28 changes: 28 additions & 0 deletions src/lib/chat/matrix-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -857,6 +857,25 @@ export class MatrixClient implements IChatClient {
return !!result.tags?.[MatrixConstants.FAVORITE];
}

async addRoomToMuted(roomId) {
await this.waitForConnection();

await this.matrix.setRoomTag(roomId, MatrixConstants.MUTE);
}

async removeRoomFromMuted(roomId) {
await this.waitForConnection();

await this.matrix.deleteRoomTag(roomId, MatrixConstants.MUTE);
}

async isRoomMuted(roomId) {
await this.waitForConnection();

const result = await this.matrix.getRoomTags(roomId);
return !!result.tags?.[MatrixConstants.MUTE];
}

async sendTypingEvent(roomId: string, isTyping: boolean): Promise<void> {
await this.waitForConnection();

Expand Down Expand Up @@ -1149,12 +1168,19 @@ export class MatrixClient implements IChatClient {

private publishRoomTagChange(event, roomId) {
const isFavoriteTagAdded = !!event.getContent().tags?.[MatrixConstants.FAVORITE];
const isMutedTagAdded = !!event.getContent().tags?.[MatrixConstants.MUTE];

if (isFavoriteTagAdded) {
this.events.roomFavorited(roomId);
} else {
this.events.roomUnfavorited(roomId);
}

if (isMutedTagAdded) {
this.events.roomMuted(roomId);
} else {
this.events.roomUnmuted(roomId);
}
}

private publishReceiptEvent(event: MatrixEvent) {
Expand All @@ -1176,6 +1202,7 @@ export class MatrixClient implements IChatClient {
const messages = await this.getAllMessagesFromRoom(room);
const unreadCount = room.getUnreadNotificationCount(NotificationCountType.Total);
const isFavorite = await this.isRoomFavorited(room.roomId);
const isMuted = await this.isRoomMuted(room.roomId);
const [admins, mods] = this.getRoomAdminsAndMods(room);

return {
Expand All @@ -1195,6 +1222,7 @@ export class MatrixClient implements IChatClient {
adminMatrixIds: admins,
moderatorIds: mods,
isFavorite,
isMuted,
};
};

Expand Down
1 change: 1 addition & 0 deletions src/lib/chat/matrix/chat-message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,7 @@ export function mapToLiveRoomEvent(liveEvent) {
id: event.event_id,
type: eventType,
createdAt: event.origin_server_ts,
roomId: event.room_id,
sender: {
userId: event.sender,
},
Expand Down
1 change: 1 addition & 0 deletions src/lib/chat/matrix/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export enum MatrixConstants {
REPLACE = 'm.replace',
BAD_ENCRYPTED_MSGTYPE = 'm.bad.encrypted',
FAVORITE = 'm.favorite',
MUTE = 'm.mute',
READ_RECEIPT_PREFERENCE = 'm.read_receipt_preference',
}

Expand Down
8 changes: 8 additions & 0 deletions src/store/channels/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ export interface Channel {
reply?: ParentMessage;
isFavorite: boolean;
otherMembersTyping: string[];
isMuted?: boolean;
}

export const CHANNEL_DEFAULTS = {
Expand All @@ -77,6 +78,7 @@ export const CHANNEL_DEFAULTS = {
moderatorIds: [],
isFavorite: false,
otherMembersTyping: [],
isMuted: false,
};

export enum SagaActionTypes {
Expand All @@ -87,6 +89,8 @@ export enum SagaActionTypes {
OnFavoriteRoom = 'channels/saga/onFavoriteRoom',
OnUnfavoriteRoom = 'channels/saga/onUnfavoriteRoom',
UserTypingInRoom = 'channels/saga/userTypingInRoom',
OnMuteRoom = 'channels/saga/onMuteRoom',
OnUnmuteRoom = 'channels/saga/onUnmuteRoom',
}

const openConversation = createAction<{ conversationId: string }>(SagaActionTypes.OpenConversation);
Expand All @@ -96,6 +100,8 @@ const onRemoveReply = createAction(SagaActionTypes.OnRemoveReply);
const onFavoriteRoom = createAction<{ roomId: string }>(SagaActionTypes.OnFavoriteRoom);
const onUnfavoriteRoom = createAction<{ roomId: string }>(SagaActionTypes.OnUnfavoriteRoom);
const userTypingInRoom = createAction<{ roomId: string }>(SagaActionTypes.UserTypingInRoom);
const onMuteRoom = createAction<{ roomId: string }>(SagaActionTypes.OnMuteRoom);
const onUnmuteRoom = createAction<{ roomId: string }>(SagaActionTypes.OnUnmuteRoom);

const slice = createNormalizedSlice({
name: 'channels',
Expand All @@ -117,4 +123,6 @@ export {
onFavoriteRoom,
onUnfavoriteRoom,
userTypingInRoom,
onMuteRoom,
onUnmuteRoom,
};
70 changes: 69 additions & 1 deletion src/store/channels/saga.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,23 @@ import {
publishUserTypingEvent,
receivedRoomMembersTyping,
receivedRoomMemberPowerLevelChanged,
roomMuted,
onMuteRoom,
roomUnmuted,
onUnmuteRoom,
} from './saga';

import { rootReducer } from '../reducer';
import { ConversationStatus, denormalize as denormalizeChannel } from '../channels';
import { StoreBuilder } from '../test/store';
import { addRoomToFavorites, chat, removeRoomFromFavorites, sendTypingEvent } from '../../lib/chat';
import {
addRoomToFavorites,
addRoomToMuted,
chat,
removeRoomFromFavorites,
removeRoomFromMuted,
sendTypingEvent,
} from '../../lib/chat';

const userId = 'user-id';

Expand Down Expand Up @@ -177,6 +188,62 @@ describe(onUnfavoriteRoom, () => {
});
});

describe(roomMuted, () => {
it('updates muted for room', async () => {
const initialState = new StoreBuilder().withConversationList({ id: 'room-id', isMuted: false }).build();
const { storeState } = await expectSaga(roomMuted, {
payload: { roomId: 'room-id' },
})
.withReducer(rootReducer, initialState)
.run();

const channel = denormalizeChannel('room-id', storeState);
expect(channel.isMuted).toEqual(true);
});
});

describe(onMuteRoom, () => {
it('calls addRoomToMuted when channel is not already muted', async () => {
const initialState = new StoreBuilder().withConversationList({ id: 'room-id', isMuted: false }).build();

await expectSaga(onMuteRoom, { payload: { roomId: 'room-id' } })
.withReducer(rootReducer, initialState)
.provide([
[matchers.call.fn(addRoomToMuted), undefined],
])
.call(addRoomToMuted, 'room-id')
.run();
});
});

describe(roomUnmuted, () => {
it('updates unmuted for room', async () => {
const initialState = new StoreBuilder().withConversationList({ id: 'room-id', isMuted: true }).build();
const { storeState } = await expectSaga(roomUnmuted, {
payload: { roomId: 'room-id' },
})
.withReducer(rootReducer, initialState)
.run();

const channel = denormalizeChannel('room-id', storeState);
expect(channel.isMuted).toEqual(false);
});
});

describe(onUnmuteRoom, () => {
it('calls removeRoomFromMuted when channel is already muted', async () => {
const initialState = new StoreBuilder().withConversationList({ id: 'channel-id', isMuted: true }).build();

await expectSaga(onUnmuteRoom, { payload: { roomId: 'channel-id' } })
.withReducer(rootReducer, initialState)
.provide([
[matchers.call.fn(removeRoomFromMuted), undefined],
])
.call(removeRoomFromMuted, 'channel-id')
.run();
});
});

describe(publishUserTypingEvent, () => {
it('sends typing event', async () => {
const initialState = new StoreBuilder().withConversationList({ id: 'room-id' }).build();
Expand Down Expand Up @@ -314,4 +381,5 @@ const CHANNEL_DEFAULTS = {
moderatorIds: [],
isFavorite: false,
otherMembersTyping: [],
isMuted: false,
};
42 changes: 42 additions & 0 deletions src/store/channels/saga.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import {
removeRoomFromFavorites,
chat,
sendTypingEvent as matrixSendUserTypingEvent,
addRoomToMuted,
removeRoomFromMuted,
} from '../../lib/chat';
import { mostRecentConversation } from '../channels-list/selectors';
import { setActiveConversation } from '../chat/saga';
Expand Down Expand Up @@ -147,6 +149,42 @@ export function* roomUnfavorited(action) {
}
}

export function* onMuteRoom(action) {
const { roomId } = action.payload;
try {
yield call(addRoomToMuted, roomId);
} catch (error) {
console.error(`Failed to mute room ${roomId}:`, error);
}
}

export function* onUnmuteRoom(action) {
const { roomId } = action.payload;
try {
yield call(removeRoomFromMuted, roomId);
} catch (error) {
console.error(`Failed to unmute room ${roomId}:`, error);
}
}

export function* roomMuted(action) {
const { roomId } = action.payload;
try {
yield call(receiveChannel, { id: roomId, isMuted: true });
} catch (error) {
console.error(`Failed to update mute status for room ${roomId}:`, error);
}
}

export function* roomUnmuted(action) {
const { roomId } = action.payload;
try {
yield call(receiveChannel, { id: roomId, isMuted: false });
} catch (error) {
console.error(`Failed to update unmute status for room ${roomId}:`, error);
}
}

export function* receivedRoomMembersTyping(action) {
const { roomId, userIds: matrixIds } = action.payload;

Expand Down Expand Up @@ -229,10 +267,14 @@ export function* saga() {
yield takeLatest(SagaActionTypes.OnRemoveReply, onRemoveReply);
yield takeLatest(SagaActionTypes.OnFavoriteRoom, onFavoriteRoom);
yield takeLatest(SagaActionTypes.OnUnfavoriteRoom, onUnfavoriteRoom);
yield takeLatest(SagaActionTypes.OnMuteRoom, onMuteRoom);
yield takeLatest(SagaActionTypes.OnUnmuteRoom, onUnmuteRoom);

yield takeEveryFromBus(yield call(getChatBus), ChatEvents.UnreadCountChanged, unreadCountUpdated);
yield takeEveryFromBus(yield call(getChatBus), ChatEvents.RoomFavorited, roomFavorited);
yield takeEveryFromBus(yield call(getChatBus), ChatEvents.RoomUnfavorited, roomUnfavorited);
yield takeEveryFromBus(yield call(getChatBus), ChatEvents.RoomMuted, roomMuted);
yield takeEveryFromBus(yield call(getChatBus), ChatEvents.RoomUnmuted, roomUnmuted);
yield takeEveryFromBus(yield call(getChatBus), ChatEvents.RoomMemberTyping, receivedRoomMembersTyping);
yield takeEveryFromBus(
yield call(getChatBus),
Expand Down
Loading