mirror of
https://github.com/RocketChat/Rocket.Chat.git
synced 2025-12-28 06:47:25 +00:00
regression: Threaded messages preview (#36165)
This commit is contained in:
parent
d6df5f4a54
commit
d6fa67e492
@ -146,7 +146,7 @@ export const createAutoTranslateMessageStreamHandler = (): ((message: ITranslate
|
||||
(!message.translations ||
|
||||
(!hasTranslationLanguageInMessage(message, language) && !hasTranslationLanguageInAttachments(message.attachments, language)))
|
||||
) {
|
||||
Messages.store.update(
|
||||
Messages.state.update(
|
||||
(record) => record._id === message._id,
|
||||
(record) => ({
|
||||
...record,
|
||||
@ -154,7 +154,7 @@ export const createAutoTranslateMessageStreamHandler = (): ((message: ITranslate
|
||||
}),
|
||||
);
|
||||
} else if (AutoTranslate.messageIdsToWait[message._id] !== undefined && subscription && subscription.autoTranslate !== true) {
|
||||
Messages.store.update(
|
||||
Messages.state.update(
|
||||
(record) => record._id === message._id,
|
||||
({ autoTranslateFetching: _, ...record }) => ({
|
||||
...record,
|
||||
@ -163,7 +163,7 @@ export const createAutoTranslateMessageStreamHandler = (): ((message: ITranslate
|
||||
);
|
||||
delete AutoTranslate.messageIdsToWait[message._id];
|
||||
} else if (message.autoTranslateFetching === true) {
|
||||
Messages.store.update(
|
||||
Messages.state.update(
|
||||
(record) => record._id === message._id,
|
||||
({ autoTranslateFetching: _, ...record }) => record,
|
||||
);
|
||||
|
||||
@ -303,7 +303,7 @@ export class E2ERoom extends Emitter {
|
||||
}
|
||||
|
||||
async decryptPendingMessages() {
|
||||
await Messages.store.updateAsync(
|
||||
await Messages.state.updateAsync(
|
||||
(record) => record.rid === this.roomId && record.t === 'e2e' && record.e2e === 'pending',
|
||||
(record) => this.decryptMessage(record),
|
||||
);
|
||||
|
||||
@ -716,7 +716,7 @@ class E2E extends Emitter {
|
||||
}
|
||||
|
||||
async decryptPendingMessages(): Promise<void> {
|
||||
await Messages.store.updateAsync(
|
||||
await Messages.state.updateAsync(
|
||||
(record) => record.t === 'e2e' && record.e2e === 'pending',
|
||||
(record) => this.decryptMessage(record),
|
||||
);
|
||||
|
||||
@ -25,7 +25,7 @@ class CachedChatRoom extends PrivateCachedCollection<IRoom> {
|
||||
}
|
||||
|
||||
private mergeWithSubscription(room: IRoom): IRoom {
|
||||
CachedChatSubscription.collection.store.update(
|
||||
CachedChatSubscription.collection.state.update(
|
||||
(record) => record.rid === room._id,
|
||||
(sub) => ({
|
||||
...sub,
|
||||
|
||||
@ -33,7 +33,7 @@ class CachedChatSubscription extends PrivateCachedCollection<SubscriptionWithRoo
|
||||
}
|
||||
|
||||
private mergeWithRoom(subscription: ISubscription): SubscriptionWithRoom {
|
||||
const room = CachedChatRoom.collection.store.find((record) => record._id === subscription.rid);
|
||||
const room = CachedChatRoom.collection.state.find((record) => record._id === subscription.rid);
|
||||
|
||||
const lastRoomUpdate = room?.lm || subscription.ts || room?.ts;
|
||||
|
||||
|
||||
@ -30,7 +30,7 @@ const processMessage = async (msg: IMessage & { ignored?: boolean }, { subscript
|
||||
};
|
||||
|
||||
export async function upsertMessage({ msg, subscription }: { msg: IMessage & { ignored?: boolean }; subscription?: ISubscription }) {
|
||||
Messages.store.store(await processMessage(msg, { subscription }));
|
||||
Messages.state.store(await processMessage(msg, { subscription }));
|
||||
}
|
||||
|
||||
export async function upsertMessageBulk({
|
||||
@ -41,7 +41,7 @@ export async function upsertMessageBulk({
|
||||
subscription?: ISubscription;
|
||||
}) {
|
||||
const processedMsgs = await Promise.all(msgs.map(async (msg) => processMessage(msg, { subscription })));
|
||||
Messages.store.storeMany(processedMsgs);
|
||||
Messages.state.storeMany(processedMsgs);
|
||||
}
|
||||
|
||||
const defaultLimit = parseInt(getConfig('roomListLimit') ?? '50') || 50;
|
||||
|
||||
@ -109,7 +109,7 @@ export abstract class CachedCollection<T extends IRocketChatRecord, U = T> {
|
||||
this.updatedAt = new Date(updatedAt);
|
||||
}
|
||||
|
||||
this.collection.store.replaceAll(deserializedRecords.filter(hasId));
|
||||
this.collection.state.replaceAll(deserializedRecords.filter(hasId));
|
||||
|
||||
this.updatedAt = data.updatedAt || this.updatedAt;
|
||||
|
||||
@ -155,7 +155,7 @@ export abstract class CachedCollection<T extends IRocketChatRecord, U = T> {
|
||||
}
|
||||
});
|
||||
|
||||
this.collection.store.storeMany(newRecords);
|
||||
this.collection.state.storeMany(newRecords);
|
||||
this.updatedAt = this.updatedAt === lastTime ? startTime : this.updatedAt;
|
||||
}
|
||||
|
||||
@ -178,7 +178,7 @@ export abstract class CachedCollection<T extends IRocketChatRecord, U = T> {
|
||||
|
||||
private save = withDebouncing({ wait: 1000 })(async () => {
|
||||
this.log('saving cache');
|
||||
const data = this.collection.store.records;
|
||||
const data = this.collection.state.records;
|
||||
await localforage.setItem(this.name, {
|
||||
updatedAt: this.updatedAt,
|
||||
version: this.version,
|
||||
@ -193,7 +193,7 @@ export abstract class CachedCollection<T extends IRocketChatRecord, U = T> {
|
||||
protected async clearCache() {
|
||||
this.log('clearing cache');
|
||||
await localforage.removeItem(this.name);
|
||||
this.collection.store.replaceAll([]);
|
||||
this.collection.state.replaceAll([]);
|
||||
}
|
||||
|
||||
protected async setupListener() {
|
||||
@ -211,11 +211,11 @@ export abstract class CachedCollection<T extends IRocketChatRecord, U = T> {
|
||||
}
|
||||
|
||||
if (action === 'removed') {
|
||||
this.collection.store.delete(newRecord);
|
||||
this.collection.state.delete(newRecord);
|
||||
} else {
|
||||
const { _id } = newRecord;
|
||||
if (!_id) return;
|
||||
this.collection.store.store(newRecord);
|
||||
this.collection.state.store(newRecord);
|
||||
}
|
||||
await this.save();
|
||||
}
|
||||
@ -257,7 +257,7 @@ export abstract class CachedCollection<T extends IRocketChatRecord, U = T> {
|
||||
const actionTime = hasUpdatedAt(newRecord) ? newRecord._updatedAt : startTime;
|
||||
changes.push({
|
||||
action: () => {
|
||||
this.collection.store.store(newRecord);
|
||||
this.collection.state.store(newRecord);
|
||||
if (actionTime > this.updatedAt) {
|
||||
this.updatedAt = actionTime;
|
||||
}
|
||||
@ -280,7 +280,7 @@ export abstract class CachedCollection<T extends IRocketChatRecord, U = T> {
|
||||
const actionTime = newRecord._deletedAt;
|
||||
changes.push({
|
||||
action: () => {
|
||||
this.collection.store.delete(newRecord);
|
||||
this.collection.state.delete(newRecord);
|
||||
if (actionTime > this.updatedAt) {
|
||||
this.updatedAt = actionTime;
|
||||
}
|
||||
|
||||
@ -89,7 +89,7 @@ export class MinimongoCollection<T extends { _id: string }> extends Mongo.Collec
|
||||
*
|
||||
* It's a convenience method to access the Zustand store directly i.e. outside of React components.
|
||||
*/
|
||||
get store() {
|
||||
get state() {
|
||||
return this.use.getState();
|
||||
}
|
||||
|
||||
|
||||
@ -10,7 +10,7 @@ Meteor.startup(() => {
|
||||
onLoggedIn(async () => {
|
||||
const { roles } = await sdk.rest.get('/v1/roles.list');
|
||||
// if a role is checked before this collection is populated, it will return undefined
|
||||
Roles.store.replaceAll(roles);
|
||||
Roles.state.replaceAll(roles);
|
||||
|
||||
Roles.ready.set(true);
|
||||
});
|
||||
|
||||
@ -15,7 +15,7 @@ export const useGoToRoom = ({ replace = false }: { replace?: boolean } = {}): ((
|
||||
return;
|
||||
}
|
||||
|
||||
const subscription: ISubscription | undefined = Subscriptions.store.find((record) => record.rid === rid);
|
||||
const subscription: ISubscription | undefined = Subscriptions.state.find((record) => record.rid === rid);
|
||||
|
||||
if (subscription) {
|
||||
roomCoordinator.openRouteLink(subscription.t, subscription, router.getSearchParameters(), { replace });
|
||||
|
||||
@ -14,7 +14,7 @@ export function useRoomQuery(
|
||||
const queryResult = useQuery({
|
||||
queryKey: roomsQueryKeys.room(rid),
|
||||
queryFn: async () => {
|
||||
const room = Rooms.store.get(rid);
|
||||
const room = Rooms.state.get(rid);
|
||||
|
||||
if (!room) {
|
||||
throw new RoomNotFoundError(undefined, { rid });
|
||||
|
||||
6
packages/mongo-adapter/jest.config.ts
Normal file
6
packages/mongo-adapter/jest.config.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import server from '@rocket.chat/jest-presets/server';
|
||||
import type { Config } from 'jest';
|
||||
|
||||
export default {
|
||||
preset: server.preset,
|
||||
} satisfies Config;
|
||||
@ -3,14 +3,18 @@
|
||||
"version": "0.0.2",
|
||||
"private": true,
|
||||
"devDependencies": {
|
||||
"@rocket.chat/jest-presets": "workspace:~",
|
||||
"eslint": "~8.45.0",
|
||||
"jest": "~29.7.0",
|
||||
"typescript": "~5.8.3"
|
||||
},
|
||||
"scripts": {
|
||||
"lint": "eslint --ext .js,.jsx,.ts,.tsx .",
|
||||
"lint:fix": "eslint --ext .js,.jsx,.ts,.tsx . --fix",
|
||||
"build": "rm -rf dist && tsc -p tsconfig.json",
|
||||
"dev": "tsc -p tsconfig.json --watch --preserveWatchOutput"
|
||||
"dev": "tsc -p tsconfig.json --watch --preserveWatchOutput",
|
||||
"test": "jest",
|
||||
"testunit": "jest"
|
||||
},
|
||||
"main": "./dist/index.js",
|
||||
"typings": "./dist/index.d.ts",
|
||||
|
||||
203
packages/mongo-adapter/src/filter.spec.ts
Normal file
203
packages/mongo-adapter/src/filter.spec.ts
Normal file
@ -0,0 +1,203 @@
|
||||
import { compileFilter } from './filter';
|
||||
|
||||
describe('compileFilter', () => {
|
||||
it('matches simple equality', () => {
|
||||
const filter = { foo: 'bar' };
|
||||
const fn = compileFilter<any>(filter);
|
||||
expect(fn({ foo: 'bar' })).toBe(true);
|
||||
expect(fn({ foo: 'baz' })).toBe(false);
|
||||
});
|
||||
|
||||
it('matches $in', () => {
|
||||
const filter = { foo: { $in: ['a', 'b'] } };
|
||||
const fn = compileFilter<any>(filter);
|
||||
expect(fn({ foo: 'a' })).toBe(true);
|
||||
expect(fn({ foo: 'b' })).toBe(true);
|
||||
expect(fn({ foo: 'c' })).toBe(false);
|
||||
});
|
||||
|
||||
it('matches $nin', () => {
|
||||
const filter = { foo: { $nin: ['a', 'b'] } };
|
||||
const fn = compileFilter<any>(filter);
|
||||
expect(fn({ foo: 'a' })).toBe(false);
|
||||
expect(fn({ foo: 'c' })).toBe(true);
|
||||
expect(fn({})).toBe(true);
|
||||
});
|
||||
|
||||
it('matches $all', () => {
|
||||
const filter = { foo: { $all: ['a', 'b'] } };
|
||||
const fn = compileFilter<any>(filter);
|
||||
expect(fn({ foo: ['a', 'b', 'c'] })).toBe(true);
|
||||
expect(fn({ foo: ['a', 'c'] })).toBe(false);
|
||||
});
|
||||
|
||||
it('matches $lt/$lte/$gt/$gte', () => {
|
||||
const filter = { foo: { $lt: 5 } };
|
||||
const fn = compileFilter<any>(filter);
|
||||
expect(fn({ foo: 4 })).toBe(true);
|
||||
expect(fn({ foo: 5 })).toBe(false);
|
||||
const filter2 = { foo: { $lte: 5 } };
|
||||
const fn2 = compileFilter<any>(filter2);
|
||||
expect(fn2({ foo: 5 })).toBe(true);
|
||||
const filter3 = { foo: { $gt: 5 } };
|
||||
const fn3 = compileFilter<any>(filter3);
|
||||
expect(fn3({ foo: 6 })).toBe(true);
|
||||
const filter4 = { foo: { $gte: 5 } };
|
||||
const fn4 = compileFilter<any>(filter4);
|
||||
expect(fn4({ foo: 5 })).toBe(true);
|
||||
});
|
||||
|
||||
it('matches $ne', () => {
|
||||
const filter = { foo: { $ne: 1 } };
|
||||
const fn = compileFilter<any>(filter);
|
||||
expect(fn({ foo: 2 })).toBe(true);
|
||||
expect(fn({ foo: 1 })).toBe(false);
|
||||
});
|
||||
|
||||
it('matches $exists', () => {
|
||||
const filter = { foo: { $exists: true } };
|
||||
const fn = compileFilter<any>(filter);
|
||||
expect(fn({ foo: 1 })).toBe(true);
|
||||
expect(fn({})).toBe(false);
|
||||
const filter2 = { foo: { $exists: false } };
|
||||
const fn2 = compileFilter<any>(filter2);
|
||||
expect(fn2({ foo: 1 })).toBe(false);
|
||||
expect(fn2({})).toBe(true);
|
||||
});
|
||||
|
||||
it('matches $mod', () => {
|
||||
const filter = { foo: { $mod: [2, 0] } };
|
||||
const fn = compileFilter<any>(filter);
|
||||
expect(fn({ foo: 4 })).toBe(true);
|
||||
expect(fn({ foo: 5 })).toBe(false);
|
||||
});
|
||||
|
||||
it('matches $size', () => {
|
||||
const filter = { foo: { $size: 2 } };
|
||||
const fn = compileFilter<any>(filter);
|
||||
expect(fn({ foo: [1, 2] })).toBe(true);
|
||||
expect(fn({ foo: [1] })).toBe(false);
|
||||
});
|
||||
|
||||
it('matches $type', () => {
|
||||
const filter = { foo: { $type: 2 } };
|
||||
const fn = compileFilter<any>(filter);
|
||||
expect(fn({ foo: 'bar' })).toBe(true);
|
||||
expect(fn({ foo: 1 })).toBe(false);
|
||||
});
|
||||
|
||||
it('matches $regex', () => {
|
||||
const filter = { foo: { $regex: '^b' } };
|
||||
const fn = compileFilter<any>(filter);
|
||||
expect(fn({ foo: 'bar' })).toBe(true);
|
||||
expect(fn({ foo: 'baz' })).toBe(true);
|
||||
expect(fn({ foo: 'car' })).toBe(false);
|
||||
});
|
||||
|
||||
it('matches $elemMatch', () => {
|
||||
const filter = { foo: { $elemMatch: { bar: 1 } } };
|
||||
const fn = compileFilter<any>(filter);
|
||||
expect(fn({ foo: [{ bar: 1 }, { bar: 2 }] })).toBe(true);
|
||||
expect(fn({ foo: [{ bar: 2 }] })).toBe(false);
|
||||
});
|
||||
|
||||
it('matches $eq', () => {
|
||||
const filter = { foo: { $eq: 1 } };
|
||||
const fn = compileFilter<any>(filter);
|
||||
expect(fn({ foo: 1 })).toBe(true);
|
||||
expect(fn({ foo: 2 })).toBe(false);
|
||||
});
|
||||
|
||||
it('matches $not', () => {
|
||||
const filter = { foo: { $not: { $eq: 1 } } };
|
||||
const fn = compileFilter<any>(filter);
|
||||
expect(fn({ foo: 2 })).toBe(true);
|
||||
expect(fn({ foo: 1 })).toBe(false);
|
||||
});
|
||||
|
||||
it('matches $and', () => {
|
||||
const filter = { $and: [{ foo: 1 }, { bar: 2 }] };
|
||||
const fn = compileFilter<any>(filter);
|
||||
expect(fn({ foo: 1, bar: 2 })).toBe(true);
|
||||
expect(fn({ foo: 1, bar: 3 })).toBe(false);
|
||||
});
|
||||
|
||||
it('matches $or', () => {
|
||||
const filter = { $or: [{ foo: 1 }, { bar: 2 }] };
|
||||
const fn = compileFilter<any>(filter);
|
||||
expect(fn({ foo: 1 })).toBe(true);
|
||||
expect(fn({ bar: 2 })).toBe(true);
|
||||
expect(fn({ foo: 3, bar: 4 })).toBe(false);
|
||||
});
|
||||
|
||||
it('matches $nor', () => {
|
||||
const filter = { $nor: [{ foo: 1 }, { bar: 2 }] };
|
||||
const fn = compileFilter<any>(filter);
|
||||
expect(fn({ foo: 1 })).toBe(false);
|
||||
expect(fn({ bar: 2 })).toBe(false);
|
||||
expect(fn({ foo: 3, bar: 4 })).toBe(true);
|
||||
});
|
||||
|
||||
it('matches $where (function)', () => {
|
||||
const filter = { $where: (doc: any) => doc.foo === 1 };
|
||||
const fn = compileFilter<any>(filter);
|
||||
expect(fn({ foo: 1 })).toBe(true);
|
||||
expect(fn({ foo: 2 })).toBe(false);
|
||||
});
|
||||
|
||||
it('matches $where (string)', () => {
|
||||
const filter = { $where: 'this.foo === 1' };
|
||||
const fn = compileFilter<any>(filter);
|
||||
expect(fn({ foo: 1 })).toBe(true);
|
||||
expect(fn({ foo: 2 })).toBe(false);
|
||||
});
|
||||
|
||||
it('handles undefined and null', () => {
|
||||
const filter = { foo: undefined, bar: null };
|
||||
const fn = compileFilter<any>(filter);
|
||||
expect(fn({})).toBe(true);
|
||||
expect(fn({ foo: null, bar: null })).toBe(true);
|
||||
expect(fn({ foo: 1, bar: 2 })).toBe(false);
|
||||
});
|
||||
|
||||
it('handles nested fields', () => {
|
||||
const filter = { 'foo.bar': 1 };
|
||||
const fn = compileFilter<any>(filter);
|
||||
expect(fn({ foo: { bar: 1 } })).toBe(true);
|
||||
expect(fn({ foo: { bar: 2 } })).toBe(false);
|
||||
});
|
||||
|
||||
it('handles arrays for $in/$all', () => {
|
||||
const filter = { foo: { $in: [1, 2] }, bar: { $all: [3, 4] } };
|
||||
const fn = compileFilter<any>(filter);
|
||||
expect(fn({ foo: 1, bar: [3, 4, 5] })).toBe(true);
|
||||
expect(fn({ foo: 3, bar: [3, 4, 5] })).toBe(false);
|
||||
});
|
||||
|
||||
it('returns true for empty filter', () => {
|
||||
const fn = compileFilter<any>({});
|
||||
expect(fn({})).toBe(true);
|
||||
expect(fn({ foo: 1 })).toBe(true);
|
||||
});
|
||||
|
||||
it('handles multiple keys', () => {
|
||||
const filter = { foo: 1, bar: 2 };
|
||||
const fn = compileFilter<any>(filter);
|
||||
expect(fn({ foo: 1, bar: 2 })).toBe(true);
|
||||
expect(fn({ foo: 1, bar: 3 })).toBe(false);
|
||||
});
|
||||
|
||||
it('handles complex nested structures', () => {
|
||||
const filter = { $or: [{ a: { $exists: false } }, { b: { $eq: true } }] };
|
||||
const fn = compileFilter<any>(filter);
|
||||
expect(fn({ a: undefined, b: true })).toBe(true);
|
||||
expect(fn({ a: '123', b: true })).toBe(true);
|
||||
expect(fn({ a: '123', b: false })).toBe(false);
|
||||
expect(fn({ a: undefined, b: false })).toBe(true);
|
||||
});
|
||||
|
||||
it('throws error for unsupported operators', () => {
|
||||
const filter = { $unsupported: 'value' };
|
||||
expect(() => compileFilter<any>(filter)).toThrow('Unrecognized logical operator: $unsupported');
|
||||
});
|
||||
});
|
||||
@ -47,6 +47,16 @@ const $all =
|
||||
return operand.every((operandElement) => value.some((valueElement) => equals(operandElement, valueElement)));
|
||||
};
|
||||
|
||||
const $eq =
|
||||
<T>(operand: T, _options: undefined): ((value: T) => boolean) =>
|
||||
(value: T): boolean => {
|
||||
if (value === undefined) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return flatSome(value, (x) => equals(x, operand));
|
||||
};
|
||||
|
||||
const $lt =
|
||||
<T>(operand: T, _options: undefined): ((value: T) => boolean) =>
|
||||
(value: T): boolean =>
|
||||
@ -130,7 +140,9 @@ const $elemMatch = <T>(operand: Filter<T>, _options: undefined): ((value: T) =>
|
||||
|
||||
const $not = <T>(operand: FieldExpression<T>, _options: undefined): ((value: T) => boolean) => {
|
||||
const matcher = compileValueSelector(operand);
|
||||
return (value: T): boolean => !matcher(value);
|
||||
return (value: T): boolean => {
|
||||
return !matcher(value);
|
||||
};
|
||||
};
|
||||
|
||||
const dummyOperator =
|
||||
@ -143,6 +155,7 @@ const $near = dummyOperator;
|
||||
const $geoIntersects = dummyOperator;
|
||||
|
||||
const valueOperators = {
|
||||
$eq,
|
||||
$in,
|
||||
$nin,
|
||||
$all,
|
||||
@ -179,8 +192,8 @@ const $nor = <T>(subSelector: Filter<T>[]): ((doc: T) => boolean) => {
|
||||
};
|
||||
|
||||
const $where = <T>(selectorValue: string | ((doc: T) => boolean)): ((doc: T) => boolean) => {
|
||||
const fn = selectorValue instanceof Function ? selectorValue : Function(`return ${selectorValue}`);
|
||||
return (doc: T): boolean => !!fn.call(doc);
|
||||
const fn = selectorValue instanceof Function ? selectorValue : Function(`doc`, `return ${selectorValue}`);
|
||||
return (doc: T): boolean => !!fn.call(doc, doc);
|
||||
};
|
||||
|
||||
const logicalOperators = {
|
||||
@ -287,6 +300,10 @@ export const compileFilter = <T>(filter: Filter<T> | FieldExpression<T>['$where'
|
||||
}
|
||||
}
|
||||
|
||||
if (key.slice(0, 1) === '$') {
|
||||
throw new Error(`Unrecognized logical operator: ${key}`);
|
||||
}
|
||||
|
||||
const lookUpByIndex = createLookupFunction(key);
|
||||
const valueSelectorFunc = compileValueSelector(subSelector);
|
||||
return (doc: T): boolean => {
|
||||
|
||||
@ -9233,7 +9233,9 @@ __metadata:
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@rocket.chat/mongo-adapter@workspace:packages/mongo-adapter"
|
||||
dependencies:
|
||||
"@rocket.chat/jest-presets": "workspace:~"
|
||||
eslint: "npm:~8.45.0"
|
||||
jest: "npm:~29.7.0"
|
||||
typescript: "npm:~5.8.3"
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
Loading…
Reference in New Issue
Block a user