[Gekidou CRT] thread mention counts (#6126)

* Sets values on my_channel according to CRT

* Team counts with regard to CRT

* Fixes myChannel.is_unread with regard to CRT

* Include DM/GMs for thread counts on demand

* Incorporate thread mention counts in server/channel

* Channel updates in regard to CRT
This commit is contained in:
Kyriakos Z
2022-04-12 13:27:40 +03:00
committed by GitHub
parent e741d47fc2
commit df3ef72a0a
15 changed files with 212 additions and 58 deletions

View File

@@ -186,7 +186,7 @@ export async function markTeamThreadsAsRead(serverUrl: string, teamId: string, p
}
try {
const {database} = operator;
const threads = await queryThreadsInTeam(database, teamId, true).fetch();
const threads = await queryThreadsInTeam(database, teamId, true, true, true).fetch();
const models = threads.map((thread) => thread.prepareUpdate((record) => {
record.unreadMentions = 0;
record.unreadReplies = 0;

View File

@@ -226,6 +226,8 @@ export async function fetchPostsForChannel(serverUrl: string, channelId: string,
}
if (!fetchOnly) {
const isCRTEnabled = await getIsCRTEnabled(operator.database);
const models = [];
const postModels = await operator.handlePosts({
actionType,
@@ -243,14 +245,18 @@ export async function fetchPostsForChannel(serverUrl: string, channelId: string,
let lastPostAt = 0;
for (const post of data.posts) {
lastPostAt = post.create_at > lastPostAt ? post.create_at : lastPostAt;
}
const {member: memberModel} = await updateLastPostAt(serverUrl, channelId, lastPostAt, true);
if (memberModel) {
models.push(memberModel);
if (!isCRTEnabled || post.root_id) {
lastPostAt = post.create_at > lastPostAt ? post.create_at : lastPostAt;
}
}
if (lastPostAt) {
const {member: memberModel} = await updateLastPostAt(serverUrl, channelId, lastPostAt, true);
if (memberModel) {
models.push(memberModel);
}
}
const isCRTEnabled = await getIsCRTEnabled(operator.database);
if (isCRTEnabled) {
const threadModels = await prepareThreadsFromReceivedPosts(operator, data.posts);
if (threadModels?.length) {
@@ -841,8 +847,18 @@ export const markPostAsUnread = async (serverUrl: string, postId: string) => {
client.getChannelMember(channelId, userId),
]);
if (channel && channelMember) {
const messageCount = channel.total_msg_count - channelMember.msg_count;
const mentionCount = channelMember.mention_count;
const isCRTEnabled = await getIsCRTEnabled(database);
let totalMessages = channel.total_msg_count;
let messages = channelMember.msg_count;
let mentionCount = channelMember.mention_count;
if (isCRTEnabled) {
totalMessages = channel.total_msg_count_root!;
messages = channelMember.msg_count_root!;
mentionCount = channelMember.mention_count_root!;
}
const messageCount = totalMessages - messages;
await markChannelAsUnread(serverUrl, channelId, messageCount, mentionCount, post.createAt);
return {
post,

View File

@@ -149,7 +149,7 @@ export async function handleNewPostEvent(serverUrl: string, msg: WebSocketMessag
if (viewedAt) {
models.push(viewedAt);
}
} else {
} else if (!isCRTEnabled || !post.root_id) {
const hasMentions = msg.data.mentions?.includes(currentUserId);
preparedMyChannelHack(myChannel);
const {member: unreadAt} = await markChannelAsUnread(
@@ -246,18 +246,36 @@ export async function handlePostDeleted(serverUrl: string, msg: WebSocketMessage
export async function handlePostUnread(serverUrl: string, msg: WebSocketMessage) {
const {channel_id: channelId, team_id: teamId} = msg.broadcast;
const {mention_count: mentionCount, msg_count: msgCount, last_viewed_at: lastViewedAt} = msg.data;
const {
mention_count: mentionCount,
mention_count_root: mentionCountRoot,
msg_count: msgCount,
msg_count_root: msgCountRoot,
last_viewed_at: lastViewedAt,
} = msg.data;
const database = DatabaseManager.serverDatabases[serverUrl]?.database;
if (!database) {
return;
}
const [myChannel, isCRTEnabled] = await Promise.all([
getMyChannel(database, channelId),
getIsCRTEnabled(database),
]);
let messages = msgCount;
let mentions = mentionCount;
if (isCRTEnabled) {
messages = msgCountRoot;
mentions = mentionCountRoot;
}
const myChannel = await getMyChannel(database, channelId);
if (!myChannel?.manuallyUnread) {
const {channels} = await fetchMyChannel(serverUrl, teamId, channelId, true);
const channel = channels?.[0];
const postNumber = channel?.total_msg_count;
const delta = postNumber ? postNumber - msgCount : msgCount;
markChannelAsUnread(serverUrl, channelId, delta, mentionCount, lastViewedAt);
const postNumber = isCRTEnabled ? channel?.total_msg_count_root : channel?.total_msg_count;
const delta = postNumber ? postNumber - messages : messages;
markChannelAsUnread(serverUrl, channelId, delta, mentions, lastViewedAt);
}
}

View File

@@ -8,6 +8,7 @@ import {switchMap} from 'rxjs/operators';
import {queryMyChannelsByTeam} from '@queries/servers/channel';
import {observeCurrentTeamId} from '@queries/servers/system';
import {observeMentionCount} from '@queries/servers/team';
import TeamItem from './team_item';
@@ -20,10 +21,6 @@ type WithTeamsArgs = WithDatabaseArgs & {
const enhance = withObservables(['myTeam'], ({myTeam, database}: WithTeamsArgs) => {
const myChannels = queryMyChannelsByTeam(database, myTeam.id).observeWithColumns(['mentions_count', 'is_unread']);
const mentionCount = myChannels.pipe(
// eslint-disable-next-line max-nested-callbacks
switchMap((val) => of$(val.reduce((acc, v) => acc + v.mentionsCount, 0))),
);
const hasUnreads = myChannels.pipe(
// eslint-disable-next-line max-nested-callbacks
switchMap((val) => of$(val.reduce((acc, v) => acc || v.isUnread, false))),
@@ -32,7 +29,7 @@ const enhance = withObservables(['myTeam'], ({myTeam, database}: WithTeamsArgs)
return {
currentTeamId: observeCurrentTeamId(database),
team: myTeam.team.observe(),
mentionCount,
mentionCount: observeMentionCount(database, myTeam.id, false),
hasUnreads,
};
});

View File

@@ -14,6 +14,7 @@ import {
transformMyChannelSettingsRecord,
} from '@database/operator/server_data_operator/transformers/channel';
import {getUniqueRawsBy} from '@database/operator/utils/general';
import {getIsCRTEnabled} from '@queries/servers/thread';
import type {HandleChannelArgs, HandleChannelInfoArgs, HandleChannelMembershipArgs, HandleMyChannelArgs, HandleMyChannelSettingsArgs} from '@typings/database/database';
import type ChannelModel from '@typings/database/models/servers/channel';
@@ -156,14 +157,19 @@ const ChannelHandler = (superclass: any) => class extends superclass {
return [];
}
const isCRTEnabled = await getIsCRTEnabled(this.database);
const channelMap = channels.reduce((result: Record<string, Channel>, channel) => {
result[channel.id] = channel;
return result;
}, {});
for (const my of myChannels) {
const channel = channelMap[my.channel_id];
if (channel) {
const msgCount = Math.max(0, channel.total_msg_count - my.msg_count);
const total = isCRTEnabled ? channel.total_msg_count_root! : channel.total_msg_count;
const myMsgCount = isCRTEnabled ? my.msg_count_root! : my.msg_count;
const msgCount = Math.max(0, total - myMsgCount);
my.msg_count = msgCount;
my.is_unread = msgCount > 0;
}

View File

@@ -4,6 +4,7 @@
import {MM_TABLES} from '@constants/database';
import {prepareBaseRecord} from '@database/operator/server_data_operator/transformers/index';
import {extractChannelDisplayName} from '@helpers/database';
import {getIsCRTEnabled} from '@queries/servers/thread';
import {OperationType} from '@typings/database/enums';
import type {TransformerArgs} from '@typings/database/database';
@@ -120,18 +121,20 @@ export const transformChannelInfoRecord = ({action, database, value}: Transforme
* @param {RecordPair} operator.value
* @returns {Promise<MyChannelModel>}
*/
export const transformMyChannelRecord = ({action, database, value}: TransformerArgs): Promise<MyChannelModel> => {
export const transformMyChannelRecord = async ({action, database, value}: TransformerArgs): Promise<MyChannelModel> => {
const raw = value.raw as ChannelMembership;
const record = value.record as MyChannelModel;
const isCreateAction = action === OperationType.CREATE;
const isCRTEnabled = await getIsCRTEnabled(database);
const fieldsMapper = (myChannel: MyChannelModel) => {
myChannel._raw.id = isCreateAction ? (raw.channel_id || myChannel.id) : record.id;
myChannel.roles = raw.roles;
myChannel.messageCount = raw.msg_count;
myChannel.messageCount = isCRTEnabled ? raw.msg_count_root! : raw.msg_count;
myChannel.isUnread = Boolean(raw.is_unread);
myChannel.mentionsCount = raw.mention_count;
myChannel.lastPostAt = raw.last_post_at || 0;
myChannel.mentionsCount = isCRTEnabled ? raw.mention_count_root! : raw.mention_count;
myChannel.lastPostAt = (isCRTEnabled ? (raw.last_root_post_at || raw.last_post_at) : raw.last_post_at) || 0;
myChannel.lastViewedAt = raw.last_viewed_at;
myChannel.viewedAt = record?.viewedAt || 0;
};

View File

@@ -2,16 +2,23 @@
// See LICENSE.txt for license information.
import {Q} from '@nozbe/watermelondb';
import {map as map$, Subscription} from 'rxjs';
import {combineLatestWith} from 'rxjs/operators';
import {MM_TABLES} from '@constants/database';
import DatabaseManager from '@database/manager';
import {observeThreadMentionCount} from '@queries/servers/thread';
import type MyChannelModel from '@typings/database/models/servers/my_channel';
import type {Subscription} from 'rxjs';
const {SERVER: {CHANNEL, MY_CHANNEL}} = MM_TABLES;
export const subscribeServerUnreadAndMentions = (serverUrl: string, observer: (myChannels: MyChannelModel[]) => void) => {
type ObserverArgs = {
myChannels: MyChannelModel[];
threadMentionCount: number;
}
export const subscribeServerUnreadAndMentions = (serverUrl: string, observer: ({myChannels, threadMentionCount}: ObserverArgs) => void) => {
const server = DatabaseManager.serverDatabases[serverUrl];
let subscription: Subscription|undefined;
@@ -19,34 +26,47 @@ export const subscribeServerUnreadAndMentions = (serverUrl: string, observer: (m
subscription = server.database.get<MyChannelModel>(MY_CHANNEL).
query(Q.on(CHANNEL, Q.where('delete_at', Q.eq(0)))).
observeWithColumns(['is_unread', 'mentions_count']).
pipe(
combineLatestWith(observeThreadMentionCount(server.database, undefined, false)),
map$(([myChannels, threadMentionCount]) => ({myChannels, threadMentionCount})),
).
subscribe(observer);
}
return subscription;
};
export const subscribeMentionsByServer = (serverUrl: string, observer: (serverUrl: string, myChannels: MyChannelModel[]) => void) => {
export const subscribeMentionsByServer = (serverUrl: string, observer: (serverUrl: string, {myChannels, threadMentionCount}: ObserverArgs) => void) => {
const server = DatabaseManager.serverDatabases[serverUrl];
let subscription: Subscription|undefined;
if (server?.database) {
subscription = server.database.
get(MY_CHANNEL).
query(Q.on(CHANNEL, Q.where('delete_at', Q.eq(0)))).
observeWithColumns(['mentions_count']).
pipe(
combineLatestWith(observeThreadMentionCount(server.database, undefined, false)),
map$(([myChannels, threadMentionCount]) => ({myChannels, threadMentionCount})),
).
subscribe(observer.bind(undefined, serverUrl));
}
return subscription;
};
export const subscribeUnreadAndMentionsByServer = (serverUrl: string, observer: (serverUrl: string, myChannels: MyChannelModel[]) => void) => {
export const subscribeUnreadAndMentionsByServer = (serverUrl: string, observer: (serverUrl: string, {myChannels, threadMentionCount}: ObserverArgs) => void) => {
const server = DatabaseManager.serverDatabases[serverUrl];
let subscription: Subscription|undefined;
if (server?.database) {
subscription = server.database.
get(MY_CHANNEL).
subscription = server.database.get<MyChannelModel>(MY_CHANNEL).
query(Q.on(CHANNEL, Q.where('delete_at', Q.eq(0)))).
observeWithColumns(['mentions_count', 'has_unreads']).
observeWithColumns(['mentions_count', 'is_unread']).
pipe(
combineLatestWith(observeThreadMentionCount(server.database, undefined, false)),
map$(([myChannels, threadMentionCount]) => ({myChannels, threadMentionCount})),
).
subscribe(observer.bind(undefined, serverUrl));
}

View File

@@ -2,8 +2,8 @@
// See LICENSE.txt for license information.
import {Database, Model, Q, Query, Relation} from '@nozbe/watermelondb';
import {of as of$} from 'rxjs';
import {switchMap} from 'rxjs/operators';
import {of as of$, Observable} from 'rxjs';
import {switchMap, distinctUntilChanged} from 'rxjs/operators';
import {General, Permissions} from '@constants';
import {MM_TABLES} from '@constants/database';
@@ -372,3 +372,26 @@ export const queryMyChannelSettingsByIds = (database: Database, ids: string[]) =
export const queryChannelsByNames = (database: Database, names: string[]) => {
return database.get<ChannelModel>(CHANNEL).query(Q.where('name', Q.oneOf(names)));
};
export function observeMyChannelMentionCount(database: Database, teamId?: string, columns = ['mentions_count', 'is_unread']): Observable<number> {
const conditions: Q.Condition[] = [
Q.where('delete_at', Q.eq(0)),
];
if (teamId) {
conditions.push(Q.where('team_id', Q.eq(teamId)));
}
return database.get<MyChannelModel>(MY_CHANNEL).query(
Q.on(CHANNEL, Q.and(
...conditions,
)),
).
observeWithColumns(columns).
pipe(
switchMap((val) => of$(val.reduce((acc, v) => {
return acc + v.mentionsCount;
}, 0))),
distinctUntilChanged(),
);
}

View File

@@ -2,8 +2,8 @@
// See LICENSE.txt for license information.
import {Database, Model, Q, Query, Relation} from '@nozbe/watermelondb';
import {of as of$} from 'rxjs';
import {switchMap} from 'rxjs/operators';
import {of as of$, map as map$, Observable} from 'rxjs';
import {switchMap, distinctUntilChanged, combineLatestWith} from 'rxjs/operators';
import {Database as DatabaseConstants, Preferences} from '@constants';
import {getPreferenceValue} from '@helpers/api/preference';
@@ -11,9 +11,10 @@ import {selectDefaultTeam} from '@helpers/api/team';
import {DEFAULT_LOCALE} from '@i18n';
import {prepareDeleteCategory} from './categories';
import {prepareDeleteChannel, getDefaultChannelForTeam} from './channel';
import {prepareDeleteChannel, getDefaultChannelForTeam, observeMyChannelMentionCount} from './channel';
import {queryPreferencesByCategoryAndName} from './preference';
import {patchTeamHistory, getConfig, getTeamHistory, observeCurrentTeamId} from './system';
import {observeThreadMentionCount} from './thread';
import {getCurrentUser} from './user';
import type ServerDataOperator from '@database/operator/server_data_operator';
@@ -21,7 +22,12 @@ import type MyTeamModel from '@typings/database/models/servers/my_team';
import type TeamModel from '@typings/database/models/servers/team';
import type TeamChannelHistoryModel from '@typings/database/models/servers/team_channel_history';
const {MY_TEAM, TEAM, TEAM_CHANNEL_HISTORY, MY_CHANNEL} = DatabaseConstants.MM_TABLES.SERVER;
const {
MY_CHANNEL,
MY_TEAM,
TEAM,
TEAM_CHANNEL_HISTORY,
} = DatabaseConstants.MM_TABLES.SERVER;
export const addChannelToTeamHistory = async (operator: ServerDataOperator, teamId: string, channelId: string, prepareRecordsOnly = false) => {
let tch: TeamChannelHistory|undefined;
@@ -377,3 +383,14 @@ export const observeCurrentTeam = (database: Database) => {
switchMap((id) => observeTeam(database, id)),
);
};
export function observeMentionCount(database: Database, teamId?: string, includeDmGm?: boolean): Observable<number> {
const channelMentionCountObservable = observeMyChannelMentionCount(database, teamId);
const threadMentionCountObservable = observeThreadMentionCount(database, teamId, includeDmGm);
return channelMentionCountObservable.pipe(
combineLatestWith(threadMentionCountObservable),
map$(([ccount, tcount]) => ccount + tcount),
distinctUntilChanged(),
);
}

View File

@@ -2,8 +2,8 @@
// See LICENSE.txt for license information.
import {Database, Q, Query} from '@nozbe/watermelondb';
import {combineLatest, of as of$} from 'rxjs';
import {map, switchMap} from 'rxjs/operators';
import {combineLatest, of as of$, Observable} from 'rxjs';
import {map, switchMap, distinctUntilChanged} from 'rxjs/operators';
import {Preferences} from '@constants';
import {MM_TABLES} from '@constants/database';
@@ -52,17 +52,24 @@ export const observeThreadById = (database: Database, threadId: string) => {
);
};
export const observeUnreadsAndMentionsInTeam = (database: Database, teamId: string) => {
return queryThreadsInTeam(database, teamId, true).observeWithColumns(['unread_replies', 'unread_mentions']).pipe(
switchMap((threads) => {
let unreads = 0;
let mentions = 0;
threads.forEach((thread) => {
unreads += thread.unreadReplies;
mentions += thread.unreadMentions;
});
return of$({unreads, mentions});
}),
export const observeUnreadsAndMentionsInTeam = (database: Database, teamId?: string, includeDmGm?: boolean): Observable<{unreads: number; mentions: number}> => {
const observeThreads = () => queryThreads(database, teamId, true, includeDmGm).
observeWithColumns(['unread_replies', 'unread_mentions']).
pipe(
switchMap((threads) => {
let unreads = 0;
let mentions = 0;
for (const thread of threads) {
unreads += thread.unreadReplies;
mentions += thread.unreadMentions;
}
return of$({unreads, mentions});
}),
);
return observeIsCRTEnabled(database).pipe(
switchMap((hasCRT) => (hasCRT ? observeThreads() : of$({unreads: 0, mentions: 0}))),
);
};
@@ -125,3 +132,47 @@ export const queryThreadsInTeam = (database: Database, teamId: string, onlyUnrea
return database.get<ThreadModel>(THREAD).query(...query);
};
export function observeThreadMentionCount(database: Database, teamId?: string, includeDmGm?: boolean): Observable<number> {
return observeUnreadsAndMentionsInTeam(database, teamId, includeDmGm).pipe(
switchMap(({mentions}) => of$(mentions)),
distinctUntilChanged(),
);
}
export const queryThreads = (database: Database, teamId?: string, onlyUnreads = false, includeDmGm = true): Query<ThreadModel> => {
const query: Q.Clause[] = [
Q.where('is_following', true),
Q.where('reply_count', Q.gt(0)),
];
// If teamId is specified, only get threads in that team
if (teamId) {
let condition: Q.Condition = Q.where('team_id', teamId);
if (includeDmGm) {
condition = Q.or(
Q.where('team_id', teamId),
Q.where('team_id', ''),
);
}
query.push(
Q.experimentalNestedJoin(POST, CHANNEL),
Q.on(POST, Q.on(CHANNEL, condition)),
);
} else if (!includeDmGm) {
// fetching all threads from all teams
// excluding DM/GM channels
query.push(
Q.experimentalNestedJoin(POST, CHANNEL),
Q.on(POST, Q.on(CHANNEL, Q.where('team_id', Q.notEq('')))),
);
}
if (onlyUnreads) {
query.push(Q.where('unread_replies', Q.gt(0)));
}
return database.get<ThreadModel>(THREAD).query(...query);
};

View File

@@ -37,7 +37,7 @@ const OtherMentionsBadge = ({channelId}: Props) => {
setCount(mentions);
};
const unreadsSubscription = (serverUrl: string, myChannels: MyChannelModel[]) => {
const unreadsSubscription = (serverUrl: string, {myChannels, threadMentionCount}: {myChannels: MyChannelModel[]; threadMentionCount: number}) => {
const unreads = subscriptions.get(serverUrl);
if (unreads) {
let mentions = 0;
@@ -47,7 +47,7 @@ const OtherMentionsBadge = ({channelId}: Props) => {
}
}
unreads.mentions = mentions;
unreads.mentions = mentions + threadMentionCount;
subscriptions.set(serverUrl, unreads);
updateCount();
}

View File

@@ -66,7 +66,7 @@ export default function Servers() {
setTotal({mentions, unread});
};
const unreadsSubscription = (serverUrl: string, myChannels: MyChannelModel[]) => {
const unreadsSubscription = (serverUrl: string, {myChannels, threadMentionCount}: {myChannels: MyChannelModel[]; threadMentionCount: number}) => {
const unreads = subscriptions.get(serverUrl);
if (unreads) {
let mentions = 0;
@@ -76,7 +76,7 @@ export default function Servers() {
unread = unread || myChannel.isUnread;
}
unreads.mentions = mentions;
unreads.mentions = mentions + threadMentionCount;
unreads.unread = unread;
subscriptions.set(serverUrl, unreads);
updateTotal();

View File

@@ -136,13 +136,14 @@ const ServerItem = ({highlight, isActive, server, tutorialWatched}: Props) => {
displayName = intl.formatMessage({id: 'servers.default', defaultMessage: 'Default Server'});
}
const unreadsSubscription = (myChannels: MyChannelModel[]) => {
const unreadsSubscription = ({myChannels, threadMentionCount}: {myChannels: MyChannelModel[]; threadMentionCount: number}) => {
let mentions = 0;
let isUnread = false;
for (const myChannel of myChannels) {
mentions += myChannel.mentionsCount;
isUnread = isUnread || myChannel.isUnread;
}
mentions += threadMentionCount;
setBadge({isUnread, mentions});
};

View File

@@ -51,7 +51,7 @@ const Home = ({isFocused, theme}: Props) => {
setTotal({mentions, unread});
};
const unreadsSubscription = (serverUrl: string, myChannels: MyChannelModel[]) => {
const unreadsSubscription = (serverUrl: string, {myChannels, threadMentionCount}: {myChannels: MyChannelModel[]; threadMentionCount: number}) => {
const unreads = subscriptions.get(serverUrl);
if (unreads) {
let mentions = 0;
@@ -61,7 +61,7 @@ const Home = ({isFocused, theme}: Props) => {
unread = unread || myChannel.isUnread;
}
unreads.mentions = mentions;
unreads.mentions = mentions + threadMentionCount;
unreads.unread = unread;
subscriptions.set(serverUrl, unreads);
updateTotal();

View File

@@ -28,6 +28,7 @@ type Channel = {
purpose: string;
last_post_at: number;
total_msg_count: number;
total_msg_count_root?: number;
extra_update_at: number;
creator_id: string;
scheme_id: string|null;
@@ -60,6 +61,7 @@ type ChannelMembership = {
mention_count_root?: number;
notify_props: Partial<ChannelNotifyProps>;
last_post_at?: number;
last_root_post_at?: number;
last_update_at: number;
scheme_user?: boolean;
scheme_admin?: boolean;