You've already forked matrix-js-sdk
mirror of
https://github.com/matrix-org/matrix-js-sdk.git
synced 2025-07-30 04:23:07 +03:00
Early directory management
This commit is contained in:
@ -23,6 +23,7 @@ import {
|
|||||||
MSC3089TreeSpace,
|
MSC3089TreeSpace,
|
||||||
TreePermissions
|
TreePermissions
|
||||||
} from "../../../src/models/MSC3089TreeSpace";
|
} from "../../../src/models/MSC3089TreeSpace";
|
||||||
|
import { DEFAULT_ALPHABET } from "../../../src/utils";
|
||||||
|
|
||||||
describe("MSC3089TreeSpace", () => {
|
describe("MSC3089TreeSpace", () => {
|
||||||
let client: MatrixClient;
|
let client: MatrixClient;
|
||||||
@ -178,4 +179,560 @@ describe("MSC3089TreeSpace", () => {
|
|||||||
it('should support defaults: Owner', () => {
|
it('should support defaults: Owner', () => {
|
||||||
return evaluatePowerLevels({}, TreePermissions.Owner, 100);
|
return evaluatePowerLevels({}, TreePermissions.Owner, 100);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should create subdirectories', async () => {
|
||||||
|
const subspaceName = "subdirectory";
|
||||||
|
const subspaceId = "!subspace:localhost";
|
||||||
|
const domain = "domain.example.com";
|
||||||
|
client.getRoom = (roomId: string) => {
|
||||||
|
if (roomId === tree.roomId) {
|
||||||
|
return tree.room;
|
||||||
|
} else if (roomId === subspaceId) {
|
||||||
|
return {} as Room; // we don't need anything important off of this
|
||||||
|
} else {
|
||||||
|
throw new Error("Unexpected getRoom call");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
client.getDomain = () => domain;
|
||||||
|
const createFn = jest.fn().mockImplementation(async (name: string) => {
|
||||||
|
expect(name).toEqual(subspaceName);
|
||||||
|
return new MSC3089TreeSpace(client, subspaceId);
|
||||||
|
});
|
||||||
|
const sendStateFn = jest.fn().mockImplementation(async (roomId: string, eventType: EventType, content: any, stateKey: string) => {
|
||||||
|
expect([tree.roomId, subspaceId]).toContain(roomId);
|
||||||
|
if (roomId === subspaceId) {
|
||||||
|
expect(eventType).toEqual(EventType.SpaceParent);
|
||||||
|
expect(stateKey).toEqual(tree.roomId);
|
||||||
|
} else {
|
||||||
|
expect(eventType).toEqual(EventType.SpaceChild);
|
||||||
|
expect(stateKey).toEqual(subspaceId);
|
||||||
|
}
|
||||||
|
expect(content).toMatchObject({via: [domain]});
|
||||||
|
|
||||||
|
// return value not used
|
||||||
|
});
|
||||||
|
client.unstableCreateFileTree = createFn;
|
||||||
|
client.sendStateEvent = sendStateFn;
|
||||||
|
|
||||||
|
const directory = await tree.createDirectory(subspaceName);
|
||||||
|
expect(directory).toBeDefined();
|
||||||
|
expect(directory).not.toBeNull();
|
||||||
|
expect(directory).not.toBe(tree);
|
||||||
|
expect(directory.roomId).toEqual(subspaceId);
|
||||||
|
expect(createFn).toHaveBeenCalledTimes(1);
|
||||||
|
expect(sendStateFn).toHaveBeenCalledTimes(2);
|
||||||
|
|
||||||
|
const content = expect.objectContaining({via: [domain]});
|
||||||
|
expect(sendStateFn).toHaveBeenCalledWith(subspaceId, EventType.SpaceParent, content, tree.roomId);
|
||||||
|
expect(sendStateFn).toHaveBeenCalledWith(tree.roomId, EventType.SpaceChild, content, subspaceId);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should find subdirectories', () => {
|
||||||
|
const firstChildRoom = "!one:example.org";
|
||||||
|
const secondChildRoom = "!two:example.org";
|
||||||
|
const thirdChildRoom = "!three:example.org"; // to ensure it doesn't end up in the subdirectories
|
||||||
|
room.currentState = {
|
||||||
|
getStateEvents: (eventType: EventType, stateKey?: string) => {
|
||||||
|
expect(eventType).toEqual(EventType.SpaceChild);
|
||||||
|
expect(stateKey).toBeUndefined();
|
||||||
|
return [
|
||||||
|
// Partial implementations of Room
|
||||||
|
{getStateKey: () => firstChildRoom},
|
||||||
|
{getStateKey: () => secondChildRoom},
|
||||||
|
{getStateKey: () => thirdChildRoom},
|
||||||
|
];
|
||||||
|
},
|
||||||
|
};
|
||||||
|
client.getRoom = () => ({} as Room); // to appease the TreeSpace constructor
|
||||||
|
|
||||||
|
const getFn = jest.fn().mockImplementation((roomId: string) => {
|
||||||
|
if (roomId === thirdChildRoom) {
|
||||||
|
throw new Error("Mock not-a-space room case called (expected)");
|
||||||
|
}
|
||||||
|
expect([firstChildRoom, secondChildRoom]).toContain(roomId);
|
||||||
|
return new MSC3089TreeSpace(client, roomId);
|
||||||
|
});
|
||||||
|
client.unstableGetFileTreeSpace = getFn;
|
||||||
|
|
||||||
|
const subdirectories = tree.getDirectories();
|
||||||
|
expect(subdirectories).toBeDefined();
|
||||||
|
expect(subdirectories.length).toBe(2);
|
||||||
|
expect(subdirectories[0].roomId).toBe(firstChildRoom);
|
||||||
|
expect(subdirectories[1].roomId).toBe(secondChildRoom);
|
||||||
|
expect(getFn).toHaveBeenCalledTimes(3);
|
||||||
|
expect(getFn).toHaveBeenCalledWith(firstChildRoom);
|
||||||
|
expect(getFn).toHaveBeenCalledWith(secondChildRoom);
|
||||||
|
expect(getFn).toHaveBeenCalledWith(thirdChildRoom); // check to make sure it tried
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should find specific directories', () => {
|
||||||
|
client.getRoom = () => ({} as Room); // to appease the TreeSpace constructor
|
||||||
|
|
||||||
|
// Only mocking used API
|
||||||
|
const firstSubdirectory = {roomId: "!first:example.org"} as any as MSC3089TreeSpace;
|
||||||
|
const searchedSubdirectory = {roomId: "!find_me:example.org"} as any as MSC3089TreeSpace;
|
||||||
|
const thirdSubdirectory = {roomId: "!third:example.org"} as any as MSC3089TreeSpace;
|
||||||
|
tree.getDirectories = () => [firstSubdirectory, searchedSubdirectory, thirdSubdirectory];
|
||||||
|
|
||||||
|
let result = tree.getDirectory(searchedSubdirectory.roomId);
|
||||||
|
expect(result).toBe(searchedSubdirectory);
|
||||||
|
|
||||||
|
result = tree.getDirectory("not a subdirectory");
|
||||||
|
expect(result).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be able to delete itself', async () => {
|
||||||
|
const delete1 = jest.fn().mockImplementation(() => Promise.resolve());
|
||||||
|
const subdir1 = {delete: delete1} as any as MSC3089TreeSpace; // mock tested bits
|
||||||
|
|
||||||
|
const delete2 = jest.fn().mockImplementation(() => Promise.resolve());
|
||||||
|
const subdir2 = {delete: delete2} as any as MSC3089TreeSpace; // mock tested bits
|
||||||
|
|
||||||
|
const joinMemberId = "@join:example.org";
|
||||||
|
const knockMemberId = "@knock:example.org";
|
||||||
|
const inviteMemberId = "@invite:example.org";
|
||||||
|
const leaveMemberId = "@leave:example.org";
|
||||||
|
const banMemberId = "@ban:example.org";
|
||||||
|
const selfUserId = "@self:example.org";
|
||||||
|
|
||||||
|
tree.getDirectories = () => [subdir1, subdir2];
|
||||||
|
room.currentState = {
|
||||||
|
getStateEvents: (eventType: EventType, stateKey?: string) => {
|
||||||
|
expect(eventType).toEqual(EventType.RoomMember);
|
||||||
|
expect(stateKey).toBeUndefined();
|
||||||
|
return [
|
||||||
|
// Partial implementations
|
||||||
|
{getContent: () => ({membership: "join"}), getStateKey: () => joinMemberId},
|
||||||
|
{getContent: () => ({membership: "knock"}), getStateKey: () => knockMemberId},
|
||||||
|
{getContent: () => ({membership: "invite"}), getStateKey: () => inviteMemberId},
|
||||||
|
{getContent: () => ({membership: "leave"}), getStateKey: () => leaveMemberId},
|
||||||
|
{getContent: () => ({membership: "ban"}), getStateKey: () => banMemberId},
|
||||||
|
|
||||||
|
// ensure we don't kick ourselves
|
||||||
|
{getContent: () => ({membership: "join"}), getStateKey: () => selfUserId},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// These two functions are tested by input expectations, so no expectations in the function bodies
|
||||||
|
const kickFn = jest.fn().mockImplementation((userId) => Promise.resolve());
|
||||||
|
const leaveFn = jest.fn().mockImplementation(() => Promise.resolve());
|
||||||
|
client.kick = kickFn;
|
||||||
|
client.leave = leaveFn;
|
||||||
|
client.getUserId = () => selfUserId;
|
||||||
|
|
||||||
|
await tree.delete();
|
||||||
|
|
||||||
|
expect(delete1).toHaveBeenCalledTimes(1);
|
||||||
|
expect(delete2).toHaveBeenCalledTimes(1);
|
||||||
|
expect(kickFn).toHaveBeenCalledTimes(3);
|
||||||
|
expect(kickFn).toHaveBeenCalledWith(tree.roomId, joinMemberId, expect.any(String));
|
||||||
|
expect(kickFn).toHaveBeenCalledWith(tree.roomId, knockMemberId, expect.any(String));
|
||||||
|
expect(kickFn).toHaveBeenCalledWith(tree.roomId, inviteMemberId, expect.any(String));
|
||||||
|
expect(leaveFn).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('get and set order', () => {
|
||||||
|
// Danger: these are partial implementations for testing purposes only
|
||||||
|
|
||||||
|
// @ts-ignore - "MatrixEvent is a value but used as a type", which is true but not important
|
||||||
|
let childState: {[roomId: string]: MatrixEvent[]} = {};
|
||||||
|
// @ts-ignore - "MatrixEvent is a value but used as a type", which is true but not important
|
||||||
|
let parentState: MatrixEvent[] = [];
|
||||||
|
let parentRoom: Room;
|
||||||
|
let childTrees: MSC3089TreeSpace[];
|
||||||
|
let rooms: {[roomId: string]: Room};
|
||||||
|
let clientSendStateFn: jest.MockedFunction<typeof client.sendStateEvent>;
|
||||||
|
const staticDomain = "static.example.org";
|
||||||
|
|
||||||
|
function addSubspace(roomId: string, createTs?: number, order?: string) {
|
||||||
|
const content = {
|
||||||
|
via: [staticDomain],
|
||||||
|
};
|
||||||
|
if (order) content['order'] = order;
|
||||||
|
parentState.push({
|
||||||
|
getType: () => EventType.SpaceChild,
|
||||||
|
getStateKey: () => roomId,
|
||||||
|
getContent: () => content,
|
||||||
|
});
|
||||||
|
childState[roomId] = [
|
||||||
|
{
|
||||||
|
getType: () => EventType.SpaceParent,
|
||||||
|
getStateKey: () => tree.roomId,
|
||||||
|
getContent: () => ({
|
||||||
|
via: [staticDomain]
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
if (createTs) {
|
||||||
|
childState[roomId].push({
|
||||||
|
getType: () => EventType.RoomCreate,
|
||||||
|
getStateKey: () => "",
|
||||||
|
getContent: () => ({}),
|
||||||
|
getTs: () => createTs,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
rooms[roomId] = makeMockChildRoom(roomId);
|
||||||
|
childTrees.push(new MSC3089TreeSpace(client, roomId));
|
||||||
|
}
|
||||||
|
|
||||||
|
function expectOrder(childRoomId: string, order: number) {
|
||||||
|
const child = childTrees.find(c => c.roomId === childRoomId);
|
||||||
|
expect(child).toBeDefined();
|
||||||
|
expect(child.getOrder()).toEqual(order);
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeMockChildRoom(roomId: string): Room {
|
||||||
|
return {
|
||||||
|
currentState: {
|
||||||
|
getStateEvents: (eventType: EventType, stateKey?: string) => {
|
||||||
|
expect([EventType.SpaceParent, EventType.RoomCreate]).toContain(eventType);
|
||||||
|
if (eventType === EventType.RoomCreate) {
|
||||||
|
expect(stateKey).toEqual("");
|
||||||
|
return childState[roomId].find(e => e.getType() === EventType.RoomCreate);
|
||||||
|
} else {
|
||||||
|
expect(stateKey).toBeUndefined();
|
||||||
|
return childState[roomId].filter(e => e.getType() === eventType);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as Room; // partial
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
childState = {};
|
||||||
|
parentState = [];
|
||||||
|
parentRoom = {
|
||||||
|
...tree.room,
|
||||||
|
roomId: tree.roomId,
|
||||||
|
currentState: {
|
||||||
|
getStateEvents: (eventType: EventType, stateKey?: string) => {
|
||||||
|
expect([EventType.SpaceChild, EventType.RoomCreate, EventType.SpaceParent]).toContain(eventType);
|
||||||
|
if (eventType === EventType.RoomCreate) {
|
||||||
|
expect(stateKey).toEqual("");
|
||||||
|
return parentState.filter(e => e.getType() === EventType.RoomCreate)[0];
|
||||||
|
} else {
|
||||||
|
if (stateKey !== undefined) {
|
||||||
|
expect(Object.keys(rooms)).toContain(stateKey);
|
||||||
|
expect(stateKey).not.toEqual(tree.roomId);
|
||||||
|
return parentState.find(e => e.getType() === eventType && e.getStateKey() === stateKey);
|
||||||
|
} // else fine
|
||||||
|
return parentState.filter(e => e.getType() === eventType);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as Room;
|
||||||
|
childTrees = [];
|
||||||
|
rooms = {};
|
||||||
|
rooms[tree.roomId] = parentRoom;
|
||||||
|
(<any>tree).room = parentRoom; // override readonly
|
||||||
|
client.getRoom = (r) => rooms[r];
|
||||||
|
|
||||||
|
clientSendStateFn = jest.fn().mockImplementation((roomId: string, eventType: EventType, content: any, stateKey: string) => {
|
||||||
|
expect(roomId).toEqual(tree.roomId);
|
||||||
|
expect(eventType).toEqual(EventType.SpaceChild);
|
||||||
|
expect(content).toMatchObject(expect.objectContaining({
|
||||||
|
via: expect.any(Array),
|
||||||
|
order: expect.any(String),
|
||||||
|
}));
|
||||||
|
expect(Object.keys(rooms)).toContain(stateKey);
|
||||||
|
expect(stateKey).not.toEqual(tree.roomId);
|
||||||
|
|
||||||
|
const stateEvent = parentState.find(e => e.getType() === eventType && e.getStateKey() === stateKey);
|
||||||
|
expect(stateEvent).toBeDefined();
|
||||||
|
stateEvent.getContent = () => content;
|
||||||
|
|
||||||
|
return Promise.resolve(); // return value not used
|
||||||
|
});
|
||||||
|
client.sendStateEvent = clientSendStateFn;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should know when something is top level', () => {
|
||||||
|
const a = "!a:example.org";
|
||||||
|
addSubspace(a);
|
||||||
|
|
||||||
|
expect(tree.isTopLevel).toBe(true);
|
||||||
|
expect(childTrees[0].isTopLevel).toBe(false); // a bit of a hack to get at this, but it's fine
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return -1 for top level spaces', () => {
|
||||||
|
// The tree is what we've defined as top level, so it should work
|
||||||
|
expect(tree.getOrder()).toEqual(-1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw when setting an order at the top level space', async () => {
|
||||||
|
try {
|
||||||
|
// The tree is what we've defined as top level, so it should work
|
||||||
|
await tree.setOrder(2);
|
||||||
|
|
||||||
|
// noinspection ExceptionCaughtLocallyJS
|
||||||
|
throw new Error("Failed to fail");
|
||||||
|
} catch (e) {
|
||||||
|
expect(e.message).toEqual("Cannot set order of top level spaces currently");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return a stable order for unordered children', () => {
|
||||||
|
const a = "!a:example.org";
|
||||||
|
const b = "!b:example.org";
|
||||||
|
const c = "!c:example.org";
|
||||||
|
|
||||||
|
// Add in reverse order to make sure it gets ordered correctly
|
||||||
|
addSubspace(c, 3);
|
||||||
|
addSubspace(b, 2);
|
||||||
|
addSubspace(a, 1);
|
||||||
|
|
||||||
|
expectOrder(a, 0);
|
||||||
|
expectOrder(b, 1);
|
||||||
|
expectOrder(c, 2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return a stable order for ordered children', () => {
|
||||||
|
const a = "!a:example.org";
|
||||||
|
const b = "!b:example.org";
|
||||||
|
const c = "!c:example.org";
|
||||||
|
|
||||||
|
// Add in reverse order to make sure it gets ordered correctly
|
||||||
|
addSubspace(a, 1, "Z");
|
||||||
|
addSubspace(b, 2, "Y");
|
||||||
|
addSubspace(c, 3, "X");
|
||||||
|
|
||||||
|
expectOrder(c, 0);
|
||||||
|
expectOrder(b, 1);
|
||||||
|
expectOrder(a, 2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return a stable order for partially ordered children', () => {
|
||||||
|
const a = "!a:example.org";
|
||||||
|
const b = "!b:example.org";
|
||||||
|
const c = "!c:example.org";
|
||||||
|
const d = "!d:example.org";
|
||||||
|
|
||||||
|
// Add in reverse order to make sure it gets ordered correctly
|
||||||
|
addSubspace(a, 1);
|
||||||
|
addSubspace(b, 2);
|
||||||
|
addSubspace(c, 3, "Y");
|
||||||
|
addSubspace(d, 4, "X");
|
||||||
|
|
||||||
|
expectOrder(d, 0);
|
||||||
|
expectOrder(c, 1);
|
||||||
|
expectOrder(b, 3); // note order diff due to room ID comparison expectation
|
||||||
|
expectOrder(a, 2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return a stable order if the create event timestamps are the same', () => {
|
||||||
|
const a = "!a:example.org";
|
||||||
|
const b = "!b:example.org";
|
||||||
|
const c = "!c:example.org";
|
||||||
|
|
||||||
|
// Add in reverse order to make sure it gets ordered correctly
|
||||||
|
addSubspace(c, 3);
|
||||||
|
addSubspace(b, 3); // same as C
|
||||||
|
addSubspace(a, 3); // same as C
|
||||||
|
|
||||||
|
expectOrder(a, 0);
|
||||||
|
expectOrder(b, 1);
|
||||||
|
expectOrder(c, 2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return a stable order if there are no known create events', () => {
|
||||||
|
const a = "!a:example.org";
|
||||||
|
const b = "!b:example.org";
|
||||||
|
const c = "!c:example.org";
|
||||||
|
|
||||||
|
// Add in reverse order to make sure it gets ordered correctly
|
||||||
|
addSubspace(c);
|
||||||
|
addSubspace(b);
|
||||||
|
addSubspace(a);
|
||||||
|
|
||||||
|
expectOrder(a, 0);
|
||||||
|
expectOrder(b, 1);
|
||||||
|
expectOrder(c, 2);
|
||||||
|
});
|
||||||
|
|
||||||
|
// XXX: These tests rely on `getOrder()` re-calculating and not caching values.
|
||||||
|
|
||||||
|
it('should allow reordering within unordered children', async () => {
|
||||||
|
const a = "!a:example.org";
|
||||||
|
const b = "!b:example.org";
|
||||||
|
const c = "!c:example.org";
|
||||||
|
|
||||||
|
// Add in reverse order to make sure it gets ordered correctly
|
||||||
|
addSubspace(c, 3);
|
||||||
|
addSubspace(b, 2);
|
||||||
|
addSubspace(a, 1);
|
||||||
|
|
||||||
|
// Order of this state is validated by other tests.
|
||||||
|
|
||||||
|
const treeA = childTrees.find(c => c.roomId === a);
|
||||||
|
expect(treeA).toBeDefined();
|
||||||
|
await treeA.setOrder(1);
|
||||||
|
|
||||||
|
expect(clientSendStateFn).toHaveBeenCalledTimes(3);
|
||||||
|
expect(clientSendStateFn).toHaveBeenCalledWith(tree.roomId, EventType.SpaceChild, expect.objectContaining({
|
||||||
|
via: [staticDomain], // should retain domain independent of client.getDomain()
|
||||||
|
|
||||||
|
// Because of how the reordering works (maintain stable ordering before moving), we end up calling this
|
||||||
|
// function twice for the same room.
|
||||||
|
order: DEFAULT_ALPHABET[0],
|
||||||
|
}), a);
|
||||||
|
expect(clientSendStateFn).toHaveBeenCalledWith(tree.roomId, EventType.SpaceChild, expect.objectContaining({
|
||||||
|
via: [staticDomain], // should retain domain independent of client.getDomain()
|
||||||
|
order: DEFAULT_ALPHABET[1],
|
||||||
|
}), b);
|
||||||
|
expect(clientSendStateFn).toHaveBeenCalledWith(tree.roomId, EventType.SpaceChild, expect.objectContaining({
|
||||||
|
via: [staticDomain], // should retain domain independent of client.getDomain()
|
||||||
|
order: DEFAULT_ALPHABET[2],
|
||||||
|
}), a);
|
||||||
|
expectOrder(a, 1);
|
||||||
|
expectOrder(b, 0);
|
||||||
|
expectOrder(c, 2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should allow reordering within ordered children', async () => {
|
||||||
|
const a = "!a:example.org";
|
||||||
|
const b = "!b:example.org";
|
||||||
|
const c = "!c:example.org";
|
||||||
|
|
||||||
|
// Add in reverse order to make sure it gets ordered correctly
|
||||||
|
addSubspace(c, 3, "Z");
|
||||||
|
addSubspace(b, 2, "X");
|
||||||
|
addSubspace(a, 1, "V");
|
||||||
|
|
||||||
|
// Order of this state is validated by other tests.
|
||||||
|
|
||||||
|
const treeA = childTrees.find(c => c.roomId === a);
|
||||||
|
expect(treeA).toBeDefined();
|
||||||
|
await treeA.setOrder(1);
|
||||||
|
|
||||||
|
expect(clientSendStateFn).toHaveBeenCalledTimes(1);
|
||||||
|
expect(clientSendStateFn).toHaveBeenCalledWith(tree.roomId, EventType.SpaceChild, expect.objectContaining({
|
||||||
|
via: [staticDomain], // should retain domain independent of client.getDomain()
|
||||||
|
order: 'Y',
|
||||||
|
}), a);
|
||||||
|
expectOrder(a, 1);
|
||||||
|
expectOrder(b, 0);
|
||||||
|
expectOrder(c, 2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should allow reordering within partially ordered children', async () => {
|
||||||
|
const a = "!a:example.org";
|
||||||
|
const b = "!b:example.org";
|
||||||
|
const c = "!c:example.org";
|
||||||
|
const d = "!d:example.org";
|
||||||
|
|
||||||
|
// Add in reverse order to make sure it gets ordered correctly
|
||||||
|
addSubspace(a, 1);
|
||||||
|
addSubspace(b, 2);
|
||||||
|
addSubspace(c, 3, "Y");
|
||||||
|
addSubspace(d, 4, "W");
|
||||||
|
|
||||||
|
// Order of this state is validated by other tests.
|
||||||
|
|
||||||
|
const treeA = childTrees.find(c => c.roomId === a);
|
||||||
|
expect(treeA).toBeDefined();
|
||||||
|
await treeA.setOrder(2);
|
||||||
|
|
||||||
|
expect(clientSendStateFn).toHaveBeenCalledTimes(1);
|
||||||
|
expect(clientSendStateFn).toHaveBeenCalledWith(tree.roomId, EventType.SpaceChild, expect.objectContaining({
|
||||||
|
via: [staticDomain], // should retain domain independent of client.getDomain()
|
||||||
|
order: 'Z',
|
||||||
|
}), a);
|
||||||
|
expectOrder(a, 2);
|
||||||
|
expectOrder(b, 3);
|
||||||
|
expectOrder(c, 1);
|
||||||
|
expectOrder(d, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should support moving upwards', async () => {
|
||||||
|
const a = "!a:example.org";
|
||||||
|
const b = "!b:example.org";
|
||||||
|
const c = "!c:example.org";
|
||||||
|
const d = "!d:example.org";
|
||||||
|
|
||||||
|
// Add in reverse order to make sure it gets ordered correctly
|
||||||
|
addSubspace(d, 4, "Z")
|
||||||
|
addSubspace(c, 3, "X");
|
||||||
|
addSubspace(b, 2, "V");
|
||||||
|
addSubspace(a, 1, "T");
|
||||||
|
|
||||||
|
// Order of this state is validated by other tests.
|
||||||
|
|
||||||
|
const treeB = childTrees.find(c => c.roomId === b);
|
||||||
|
expect(treeB).toBeDefined();
|
||||||
|
await treeB.setOrder(2);
|
||||||
|
|
||||||
|
expect(clientSendStateFn).toHaveBeenCalledTimes(1);
|
||||||
|
expect(clientSendStateFn).toHaveBeenCalledWith(tree.roomId, EventType.SpaceChild, expect.objectContaining({
|
||||||
|
via: [staticDomain], // should retain domain independent of client.getDomain()
|
||||||
|
order: 'Y',
|
||||||
|
}), b);
|
||||||
|
expectOrder(a, 0);
|
||||||
|
expectOrder(b, 2);
|
||||||
|
expectOrder(c, 1);
|
||||||
|
expectOrder(d, 3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should support moving downwards', async () => {
|
||||||
|
const a = "!a:example.org";
|
||||||
|
const b = "!b:example.org";
|
||||||
|
const c = "!c:example.org";
|
||||||
|
const d = "!d:example.org";
|
||||||
|
|
||||||
|
// Add in reverse order to make sure it gets ordered correctly
|
||||||
|
addSubspace(d, 4, "Z")
|
||||||
|
addSubspace(c, 3, "X");
|
||||||
|
addSubspace(b, 2, "V");
|
||||||
|
addSubspace(a, 1, "T");
|
||||||
|
|
||||||
|
// Order of this state is validated by other tests.
|
||||||
|
|
||||||
|
const treeC = childTrees.find(ch => ch.roomId === c);
|
||||||
|
expect(treeC).toBeDefined();
|
||||||
|
await treeC.setOrder(1);
|
||||||
|
|
||||||
|
expect(clientSendStateFn).toHaveBeenCalledTimes(1);
|
||||||
|
expect(clientSendStateFn).toHaveBeenCalledWith(tree.roomId, EventType.SpaceChild, expect.objectContaining({
|
||||||
|
via: [staticDomain], // should retain domain independent of client.getDomain()
|
||||||
|
order: 'U',
|
||||||
|
}), c);
|
||||||
|
expectOrder(a, 0);
|
||||||
|
expectOrder(b, 2);
|
||||||
|
expectOrder(c, 1);
|
||||||
|
expectOrder(d, 3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should support moving over the partial ordering boundary', async () => {
|
||||||
|
const a = "!a:example.org";
|
||||||
|
const b = "!b:example.org";
|
||||||
|
const c = "!c:example.org";
|
||||||
|
const d = "!d:example.org";
|
||||||
|
|
||||||
|
// Add in reverse order to make sure it gets ordered correctly
|
||||||
|
addSubspace(d, 4)
|
||||||
|
addSubspace(c, 3);
|
||||||
|
addSubspace(b, 2, "V");
|
||||||
|
addSubspace(a, 1, "T");
|
||||||
|
|
||||||
|
// Order of this state is validated by other tests.
|
||||||
|
|
||||||
|
const treeB = childTrees.find(ch => ch.roomId === b);
|
||||||
|
expect(treeB).toBeDefined();
|
||||||
|
await treeB.setOrder(2);
|
||||||
|
|
||||||
|
expect(clientSendStateFn).toHaveBeenCalledTimes(2);
|
||||||
|
expect(clientSendStateFn).toHaveBeenCalledWith(tree.roomId, EventType.SpaceChild, expect.objectContaining({
|
||||||
|
via: [staticDomain], // should retain domain independent of client.getDomain()
|
||||||
|
order: 'W',
|
||||||
|
}), c);
|
||||||
|
expect(clientSendStateFn).toHaveBeenCalledWith(tree.roomId, EventType.SpaceChild, expect.objectContaining({
|
||||||
|
via: [staticDomain], // should retain domain independent of client.getDomain()
|
||||||
|
order: 'X',
|
||||||
|
}), b);
|
||||||
|
expectOrder(a, 0);
|
||||||
|
expectOrder(b, 2);
|
||||||
|
expectOrder(c, 1);
|
||||||
|
expectOrder(d, 3);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,4 +1,12 @@
|
|||||||
import * as utils from "../../src/utils";
|
import * as utils from "../../src/utils";
|
||||||
|
import {
|
||||||
|
alphabetPad,
|
||||||
|
averageBetweenStrings,
|
||||||
|
baseToString,
|
||||||
|
DEFAULT_ALPHABET,
|
||||||
|
nextString, prevString,
|
||||||
|
stringToBase
|
||||||
|
} from "../../src/utils";
|
||||||
|
|
||||||
describe("utils", function() {
|
describe("utils", function() {
|
||||||
describe("encodeParams", function() {
|
describe("encodeParams", function() {
|
||||||
@ -259,4 +267,73 @@ describe("utils", function() {
|
|||||||
expect(promiseCount).toEqual(2);
|
expect(promiseCount).toEqual(2);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('DEFAULT_ALPHABET', () => {
|
||||||
|
it('should be usefully printable ASCII in order', () => {
|
||||||
|
expect(DEFAULT_ALPHABET).toEqual(" !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('alphabetPad', () => {
|
||||||
|
it('should pad to the alphabet length', () => {
|
||||||
|
const defaultPrefixFor1char = [""].reduce(() => {
|
||||||
|
let s = "";
|
||||||
|
for (let i = 0; i < DEFAULT_ALPHABET.length - 1; i++) {
|
||||||
|
s += DEFAULT_ALPHABET[0];
|
||||||
|
}
|
||||||
|
return s;
|
||||||
|
}, "");
|
||||||
|
expect(alphabetPad("a")).toEqual(defaultPrefixFor1char + "a");
|
||||||
|
expect(alphabetPad("a", "123")).toEqual("11a");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('baseToString', () => {
|
||||||
|
it('should calculate the appropriate string from numbers', () => {
|
||||||
|
expect(baseToString(10)).toEqual(DEFAULT_ALPHABET[10]);
|
||||||
|
expect(baseToString(10, "abcdefghijklmnopqrstuvwxyz")).toEqual('k');
|
||||||
|
expect(baseToString(6241)).toEqual("ab");
|
||||||
|
expect(baseToString(53, "abcdefghijklmnopqrstuvwxyz")).toEqual('cb');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('stringToBase', () => {
|
||||||
|
it('should calculate the appropriate number for a string', () => {
|
||||||
|
expect(stringToBase(" ")).toEqual(0);
|
||||||
|
expect(stringToBase("a", "abcdefghijklmnopqrstuvwxyz")).toEqual(0);
|
||||||
|
expect(stringToBase("a")).toEqual(65);
|
||||||
|
expect(stringToBase("c", "abcdefghijklmnopqrstuvwxyz")).toEqual(2);
|
||||||
|
expect(stringToBase("ab")).toEqual(6241);
|
||||||
|
expect(stringToBase("cb", "abcdefghijklmnopqrstuvwxyz")).toEqual(53);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('averageBetweenStrings', () => {
|
||||||
|
it('should average appropriately', () => {
|
||||||
|
expect(averageBetweenStrings('A', 'z')).toEqual(']');
|
||||||
|
expect(averageBetweenStrings('a', 'z', "abcdefghijklmnopqrstuvwxyz")).toEqual('m');
|
||||||
|
expect(averageBetweenStrings('AA', 'zz')).toEqual('^.');
|
||||||
|
expect(averageBetweenStrings('aa', 'zz', "abcdefghijklmnopqrstuvwxyz")).toEqual('mz');
|
||||||
|
expect(averageBetweenStrings('cat', 'doggo')).toEqual("BH65B");
|
||||||
|
expect(averageBetweenStrings('cat', 'doggo', "abcdefghijklmnopqrstuvwxyz")).toEqual("buedq");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('nextString', () => {
|
||||||
|
it('should find the next string appropriately', () => {
|
||||||
|
expect(nextString('A')).toEqual('B');
|
||||||
|
expect(nextString('b', 'abcdefghijklmnopqrstuvwxyz')).toEqual('c');
|
||||||
|
expect(nextString('cat')).toEqual('cau');
|
||||||
|
expect(nextString('cat', 'abcdefghijklmnopqrstuvwxyz')).toEqual('cau');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('prevString', () => {
|
||||||
|
it('should find the next string appropriately', () => {
|
||||||
|
expect(prevString('B')).toEqual('A');
|
||||||
|
expect(prevString('c', 'abcdefghijklmnopqrstuvwxyz')).toEqual('b');
|
||||||
|
expect(prevString('cau')).toEqual('cat');
|
||||||
|
expect(prevString('cau', 'abcdefghijklmnopqrstuvwxyz')).toEqual('cat');
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -17,6 +17,9 @@ limitations under the License.
|
|||||||
import { MatrixClient } from "../client";
|
import { MatrixClient } from "../client";
|
||||||
import { EventType } from "../@types/event";
|
import { EventType } from "../@types/event";
|
||||||
import { Room } from "./room";
|
import { Room } from "./room";
|
||||||
|
import { logger } from "../logger";
|
||||||
|
import { MatrixEvent } from "./event";
|
||||||
|
import { averageBetweenStrings, DEFAULT_ALPHABET, nextString, prevString } from "../utils";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The recommended defaults for a tree space's power levels. Note that this
|
* The recommended defaults for a tree space's power levels. Note that this
|
||||||
@ -47,6 +50,7 @@ export const DEFAULT_TREE_POWER_LEVELS_TEMPLATE = {
|
|||||||
[EventType.RoomMessageEncrypted]: 50,
|
[EventType.RoomMessageEncrypted]: 50,
|
||||||
[EventType.Sticker]: 50,
|
[EventType.Sticker]: 50,
|
||||||
},
|
},
|
||||||
|
|
||||||
users: {}, // defined by calling code
|
users: {}, // defined by calling code
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -70,6 +74,8 @@ export class MSC3089TreeSpace {
|
|||||||
|
|
||||||
public constructor(private client: MatrixClient, public readonly roomId: string) {
|
public constructor(private client: MatrixClient, public readonly roomId: string) {
|
||||||
this.room = this.client.getRoom(this.roomId);
|
this.room = this.client.getRoom(this.roomId);
|
||||||
|
|
||||||
|
if (!this.room) throw new Error("Unknown room");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -79,6 +85,15 @@ export class MSC3089TreeSpace {
|
|||||||
return this.roomId;
|
return this.roomId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether or not this is a top level space.
|
||||||
|
*/
|
||||||
|
public get isTopLevel(): boolean {
|
||||||
|
const parentEvents = this.room.currentState.getStateEvents(EventType.SpaceParent);
|
||||||
|
if (!parentEvents?.length) return true;
|
||||||
|
return parentEvents.every(e => !e.getContent()?.['via']);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the name of the tree space.
|
* Sets the name of the tree space.
|
||||||
* @param {string} name The new name for the space.
|
* @param {string} name The new name for the space.
|
||||||
@ -135,4 +150,232 @@ export class MSC3089TreeSpace {
|
|||||||
|
|
||||||
return this.client.sendStateEvent(this.roomId, EventType.RoomPowerLevels, pls, "");
|
return this.client.sendStateEvent(this.roomId, EventType.RoomPowerLevels, pls, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a directory under this tree space, represented as another tree space.
|
||||||
|
* @param {string} name The name for the directory.
|
||||||
|
* @returns {Promise<MSC3089TreeSpace>} Resolves to the created directory.
|
||||||
|
*/
|
||||||
|
public async createDirectory(name: string): Promise<MSC3089TreeSpace> {
|
||||||
|
const directory = await this.client.unstableCreateFileTree(name);
|
||||||
|
|
||||||
|
await this.client.sendStateEvent(this.roomId, EventType.SpaceChild, {
|
||||||
|
via: [this.client.getDomain()],
|
||||||
|
}, directory.roomId);
|
||||||
|
|
||||||
|
await this.client.sendStateEvent(directory.roomId, EventType.SpaceParent, {
|
||||||
|
via: [this.client.getDomain()],
|
||||||
|
}, this.roomId);
|
||||||
|
|
||||||
|
return directory;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a list of all known immediate subdirectories to this tree space.
|
||||||
|
* @returns {MSC3089TreeSpace[]} The tree spaces (directories). May be empty, but not null.
|
||||||
|
*/
|
||||||
|
public getDirectories(): MSC3089TreeSpace[] {
|
||||||
|
const trees: MSC3089TreeSpace[] = [];
|
||||||
|
const children = this.room.currentState.getStateEvents(EventType.SpaceChild);
|
||||||
|
for (const child of children) {
|
||||||
|
try {
|
||||||
|
const tree = this.client.unstableGetFileTreeSpace(child.getStateKey());
|
||||||
|
trees.push(tree);
|
||||||
|
} catch (e) {
|
||||||
|
logger.warn("Unable to create tree space instance for listing. Are we joined?", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return trees;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a subdirectory of a given ID under this tree space. Note that this will not recurse
|
||||||
|
* into children and instead only look one level deep.
|
||||||
|
* @param {string} roomId The room ID (directory ID) to find.
|
||||||
|
* @returns {MSC3089TreeSpace} The directory, or falsy if not found.
|
||||||
|
*/
|
||||||
|
public getDirectory(roomId: string): MSC3089TreeSpace {
|
||||||
|
return this.getDirectories().find(r => r.roomId === roomId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes the tree, kicking all members and deleting **all subdirectories**.
|
||||||
|
* @returns {Promise<void>} Resolves when complete.
|
||||||
|
*/
|
||||||
|
public async delete(): Promise<void> {
|
||||||
|
const subdirectories = this.getDirectories();
|
||||||
|
for (const dir of subdirectories) {
|
||||||
|
await dir.delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
const kickMemberships = ["invite", "knock", "join"];
|
||||||
|
const members = this.room.currentState.getStateEvents(EventType.RoomMember);
|
||||||
|
for (const member of members) {
|
||||||
|
if (member.getStateKey() !== this.client.getUserId() && kickMemberships.includes(member.getContent()['membership'])) {
|
||||||
|
await this.client.kick(this.roomId, member.getStateKey(), "Room deleted");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.client.leave(this.roomId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getOrderedChildren(children: MatrixEvent[]): {roomId: string, order: string}[] {
|
||||||
|
const ordered: {roomId: string, order: string}[] = children
|
||||||
|
.map(c => ({roomId: c.getStateKey(), order: c.getContent()['order']}));
|
||||||
|
ordered.sort((a, b) => {
|
||||||
|
if (a.order && !b.order) {
|
||||||
|
return -1;
|
||||||
|
} else if (!a.order && b.order) {
|
||||||
|
return 1;
|
||||||
|
} else if (!a.order && !b.order) {
|
||||||
|
const roomA = this.client.getRoom(a.roomId);
|
||||||
|
const roomB = this.client.getRoom(b.roomId);
|
||||||
|
if (!roomA || !roomB) { // just don't bother trying to do more partial sorting
|
||||||
|
return a.roomId.localeCompare(b.roomId);
|
||||||
|
}
|
||||||
|
|
||||||
|
const createTsA = roomA.currentState.getStateEvents(EventType.RoomCreate, "")?.getTs() ?? 0;
|
||||||
|
const createTsB = roomB.currentState.getStateEvents(EventType.RoomCreate, "")?.getTs() ?? 0;
|
||||||
|
if (createTsA === createTsB) {
|
||||||
|
return a.roomId.localeCompare(b.roomId);
|
||||||
|
}
|
||||||
|
return createTsA - createTsB;
|
||||||
|
} else { // both not-null orders
|
||||||
|
return a.order.localeCompare(b.order);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return ordered;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getParentRoom(): Room {
|
||||||
|
const parents = this.room.currentState.getStateEvents(EventType.SpaceParent);
|
||||||
|
const parent = parents[0]; // XXX: Wild assumption
|
||||||
|
if (!parent) throw new Error("Expected to have a parent in a non-top level space");
|
||||||
|
|
||||||
|
// XXX: We are assuming the parent is a valid tree space.
|
||||||
|
// We probably don't need to validate the parent room state for this usecase though.
|
||||||
|
const parentRoom = this.client.getRoom(parent.getStateKey());
|
||||||
|
if (!parentRoom) throw new Error("Unable to locate room for parent");
|
||||||
|
|
||||||
|
return parentRoom;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the current order index for this directory. Note that if this is the top level space
|
||||||
|
* then -1 will be returned.
|
||||||
|
* @returns {number} The order index of this space.
|
||||||
|
*/
|
||||||
|
public getOrder(): number {
|
||||||
|
if (this.isTopLevel) return -1;
|
||||||
|
|
||||||
|
const parentRoom = this.getParentRoom();
|
||||||
|
const children = parentRoom.currentState.getStateEvents(EventType.SpaceChild);
|
||||||
|
const ordered = this.getOrderedChildren(children);
|
||||||
|
|
||||||
|
return ordered.findIndex(c => c.roomId === this.roomId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the order index for this directory within its parent. Note that if this is a top level
|
||||||
|
* space then an error will be thrown. -1 can be used to move the child to the start, and numbers
|
||||||
|
* larger than the number of children can be used to move the child to the end.
|
||||||
|
* @param {number} index The new order index for this space.
|
||||||
|
* @returns {Promise<void>} Resolves when complete.
|
||||||
|
* @throws Throws if this is a top level space.
|
||||||
|
*/
|
||||||
|
public async setOrder(index: number): Promise<void> {
|
||||||
|
if (this.isTopLevel) throw new Error("Cannot set order of top level spaces currently");
|
||||||
|
|
||||||
|
const parentRoom = this.getParentRoom();
|
||||||
|
const children = parentRoom.currentState.getStateEvents(EventType.SpaceChild);
|
||||||
|
const ordered = this.getOrderedChildren(children);
|
||||||
|
index = Math.max(Math.min(index, ordered.length - 1), 0);
|
||||||
|
|
||||||
|
const currentIndex = this.getOrder();
|
||||||
|
const movingUp = currentIndex < index;
|
||||||
|
if (movingUp && index === (ordered.length - 1)) {
|
||||||
|
index--;
|
||||||
|
} else if (!movingUp && index === 0) {
|
||||||
|
index++;
|
||||||
|
}
|
||||||
|
|
||||||
|
const prev = ordered[movingUp ? index : (index - 1)];
|
||||||
|
const next = ordered[movingUp ? (index + 1) : index];
|
||||||
|
|
||||||
|
let newOrder = DEFAULT_ALPHABET[0];
|
||||||
|
let ensureBeforeIsSane = false;
|
||||||
|
if (!prev) {
|
||||||
|
// Move to front
|
||||||
|
if (next?.order) {
|
||||||
|
newOrder = prevString(next.order);
|
||||||
|
}
|
||||||
|
} else if (index === (ordered.length - 1)) {
|
||||||
|
// Move to back
|
||||||
|
if (next?.order) {
|
||||||
|
newOrder = nextString(next.order);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Move somewhere in the middle
|
||||||
|
const startOrder = prev?.order;
|
||||||
|
const endOrder = next?.order;
|
||||||
|
if (startOrder && endOrder) {
|
||||||
|
if (startOrder === endOrder) {
|
||||||
|
// Error case: just move +1 to break out of awful math
|
||||||
|
newOrder = nextString(startOrder);
|
||||||
|
} else {
|
||||||
|
newOrder = averageBetweenStrings(startOrder, endOrder);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (startOrder) {
|
||||||
|
// We're at the end (endOrder is null, so no explicit order)
|
||||||
|
newOrder = nextString(startOrder);
|
||||||
|
} else if (endOrder) {
|
||||||
|
// We're at the start (startOrder is null, so nothing before us)
|
||||||
|
newOrder = prevString(endOrder);
|
||||||
|
} else {
|
||||||
|
// Both points are unknown. We're likely in a range where all the children
|
||||||
|
// don't have particular order values, so we may need to update them too.
|
||||||
|
// The other possibility is there's only us as a child, but we should have
|
||||||
|
// shown up in the other states.
|
||||||
|
ensureBeforeIsSane = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ensureBeforeIsSane) {
|
||||||
|
// We were asked by the order algorithm to prepare the moving space for a landing
|
||||||
|
// in the undefined order part of the order array, which means we need to update the
|
||||||
|
// spaces that come before it with a stable order value.
|
||||||
|
let lastOrder: string;
|
||||||
|
for (let i = 0; i <= index; i++) {
|
||||||
|
const target = ordered[i];
|
||||||
|
if (i === 0) {
|
||||||
|
lastOrder = target.order;
|
||||||
|
}
|
||||||
|
if (!target.order) {
|
||||||
|
// XXX: We should be creating gaps to avoid conflicts
|
||||||
|
lastOrder = lastOrder ? nextString(lastOrder) : DEFAULT_ALPHABET[0];
|
||||||
|
const currentChild = parentRoom.currentState.getStateEvents(EventType.SpaceChild, target.roomId);
|
||||||
|
const content = currentChild?.getContent() ?? {via: [this.client.getDomain()]};
|
||||||
|
await this.client.sendStateEvent(parentRoom.roomId, EventType.SpaceChild, {
|
||||||
|
...content,
|
||||||
|
order: lastOrder,
|
||||||
|
}, target.roomId);
|
||||||
|
} else {
|
||||||
|
lastOrder = target.order;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
newOrder = nextString(lastOrder);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Deal with order conflicts by reordering
|
||||||
|
|
||||||
|
// Now we can finally update our own order state
|
||||||
|
const currentChild = parentRoom.currentState.getStateEvents(EventType.SpaceChild, this.roomId);
|
||||||
|
const content = currentChild?.getContent() ?? {via: [this.client.getDomain()]};
|
||||||
|
await this.client.sendStateEvent(parentRoom.roomId, EventType.SpaceChild, {
|
||||||
|
...content,
|
||||||
|
order: newOrder,
|
||||||
|
}, this.roomId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
105
src/utils.ts
105
src/utils.ts
@ -456,3 +456,108 @@ export function setCrypto(c: Object) {
|
|||||||
export function getCrypto(): Object {
|
export function getCrypto(): Object {
|
||||||
return crypto;
|
return crypto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// String averaging based upon https://stackoverflow.com/a/2510816
|
||||||
|
// Dev note: We make the alphabet a string because it's easier to write syntactically
|
||||||
|
// than arrays. Thankfully, strings implement the useful parts of the Array interface
|
||||||
|
// anyhow.
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The default alphabet used by string averaging in this SDK. This matches
|
||||||
|
* all usefully printable ASCII characters (0x20-0x7E, inclusive).
|
||||||
|
*/
|
||||||
|
export const DEFAULT_ALPHABET = [""].reduce(() => {
|
||||||
|
let str = "";
|
||||||
|
for (let c = 0x20; c <= 0x7E; c++) {
|
||||||
|
str += String.fromCharCode(c);
|
||||||
|
}
|
||||||
|
return str;
|
||||||
|
}, "");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pads a string using the given alphabet as a base. The returned string will be the
|
||||||
|
* same length as the alphabet, and padded with the first character in the alphabet.
|
||||||
|
*
|
||||||
|
* This is intended for use with string averaging.
|
||||||
|
* @param {string} s The string to pad.
|
||||||
|
* @param {string} alphabet The alphabet to use as a single string.
|
||||||
|
* @returns {string} The padded string.
|
||||||
|
*/
|
||||||
|
export function alphabetPad(s: string, alphabet = DEFAULT_ALPHABET): string {
|
||||||
|
while (s.length < alphabet.length) {
|
||||||
|
s = alphabet[0] + s;
|
||||||
|
}
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a baseN number to a string, where N is the alphabet's length.
|
||||||
|
*
|
||||||
|
* This is intended for use with string averaging.
|
||||||
|
* @param {number} n The baseN number.
|
||||||
|
* @param {string} alphabet The alphabet to use as a single string.
|
||||||
|
* @returns {string} The baseN number encoded as a string from the alphabet.
|
||||||
|
*/
|
||||||
|
export function baseToString(n: number, alphabet = DEFAULT_ALPHABET): string {
|
||||||
|
const len = alphabet.length;
|
||||||
|
if (n < len) {
|
||||||
|
return alphabet[n];
|
||||||
|
}
|
||||||
|
return baseToString(Math.floor(n / len), alphabet) + alphabet[n % len];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a string to a baseN number, where N is the alphabet's length.
|
||||||
|
*
|
||||||
|
* This is intended for use with string averaging.
|
||||||
|
* @param {string} s The string to convert to a number.
|
||||||
|
* @param {string} alphabet The alphabet to use as a single string.
|
||||||
|
* @returns {number} The baseN number.
|
||||||
|
*/
|
||||||
|
export function stringToBase(s: string, alphabet = DEFAULT_ALPHABET): number {
|
||||||
|
s = alphabetPad(s, alphabet);
|
||||||
|
const len = alphabet.length;
|
||||||
|
const reversedStr = Array.from(s).reverse();
|
||||||
|
let result = 0;
|
||||||
|
for (let i = 0; i < len; i++) {
|
||||||
|
result += alphabet.indexOf(reversedStr[i]) * (len ** i);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Averages two strings, returning the midpoint between them. This is accomplished by
|
||||||
|
* converting both to baseN numbers (where N is the alphabet's length) then averaging
|
||||||
|
* those before re-encoding as a string.
|
||||||
|
* @param {string} a The first string.
|
||||||
|
* @param {string} b The second string.
|
||||||
|
* @param {string} alphabet The alphabet to use as a single string.
|
||||||
|
* @returns {string} The midpoint between the strings, as a string.
|
||||||
|
*/
|
||||||
|
export function averageBetweenStrings(a: string, b: string, alphabet = DEFAULT_ALPHABET): string {
|
||||||
|
return baseToString(Math.floor((stringToBase(a, alphabet) + stringToBase(b, alphabet)) / 2), alphabet);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds the next string using the alphabet provided. This is done by converting the
|
||||||
|
* string to a baseN number, where N is the alphabet's length, then adding 1 before
|
||||||
|
* converting back to a string.
|
||||||
|
* @param {string} s The string to start at.
|
||||||
|
* @param {string} alphabet The alphabet to use as a single string.
|
||||||
|
* @returns {string} The string which follows the input string.
|
||||||
|
*/
|
||||||
|
export function nextString(s: string, alphabet = DEFAULT_ALPHABET): string {
|
||||||
|
return baseToString(stringToBase(s, alphabet) + 1, alphabet);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds the previous string using the alphabet provided. This is done by converting the
|
||||||
|
* string to a baseN number, where N is the alphabet's length, then subtracting 1 before
|
||||||
|
* converting back to a string.
|
||||||
|
* @param {string} s The string to start at.
|
||||||
|
* @param {string} alphabet The alphabet to use as a single string.
|
||||||
|
* @returns {string} The string which precedes the input string.
|
||||||
|
*/
|
||||||
|
export function prevString(s: string, alphabet = DEFAULT_ALPHABET): string {
|
||||||
|
return baseToString(stringToBase(s, alphabet) - 1, alphabet);
|
||||||
|
}
|
||||||
|
Reference in New Issue
Block a user