<commit_before>/*
Copyright 2018, 2019 New Vector Ltd
Copyright 2020 The Matrix.org Foundation C.I.C.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

import { Room } from "matrix-js-sdk/src/models/room";
import { RoomUpdateCause, TagID } from "../../models";
import { SortAlgorithm } from "../models";
import { sortRoomsWithAlgorithm } from "../tag-sorting";
import { OrderingAlgorithm } from "./OrderingAlgorithm";
import { NotificationColor } from "../../../notifications/NotificationColor";
import { RoomNotificationStateStore } from "../../../notifications/RoomNotificationStateStore";

interface ICategorizedRoomMap {
    // @ts-ignore - TS wants this to be a string, but we know better
    [category: NotificationColor]: Room[];
}

interface ICategoryIndex {
    // @ts-ignore - TS wants this to be a string, but we know better
    [category: NotificationColor]: number; // integer
}

// Caution: changing this means you'll need to update a bunch of assumptions and
// comments! Check the usage of Category carefully to figure out what needs changing
// if you're going to change this array's order.
const CATEGORY_ORDER = [
    NotificationColor.Red,
    NotificationColor.Grey,
    NotificationColor.Bold,
    NotificationColor.None, // idle
];

/**
 * An implementation of the "importance" algorithm for room list sorting. Where
 * the tag sorting algorithm does not interfere, rooms will be ordered into
 * categories of varying importance to the user. Alphabetical sorting does not
 * interfere with this algorithm, however manual ordering does.
 *
 * The importance of a room is defined by the kind of notifications, if any, are
 * present on the room. These are classified internally as Red, Grey, Bold, and
 * Idle. Red rooms have mentions, grey have unread messages, bold is a less noisy
 * version of grey, and idle means all activity has been seen by the user.
 *
 * The algorithm works by monitoring all room changes, including new messages in
 * tracked rooms, to determine if it needs a new category or different placement
 * within the same category. For more information, see the comments contained
 * within the class.
 */
export class ImportanceAlgorithm extends OrderingAlgorithm {
    // This tracks the category for the tag it represents by tracking the index of
    // each category within the list, where zero is the top of the list. This then
    // tracks when rooms change categories and splices the orderedRooms array as
    // needed, preventing many ordering operations.

    private indices: ICategoryIndex = {};

    public constructor(tagId: TagID, initialSortingAlgorithm: SortAlgorithm) {
        super(tagId, initialSortingAlgorithm);
    }

    // noinspection JSMethodCanBeStatic
    private categorizeRooms(rooms: Room[]): ICategorizedRoomMap {
        const map: ICategorizedRoomMap = {
            [NotificationColor.Red]: [],
            [NotificationColor.Grey]: [],
            [NotificationColor.Bold]: [],
            [NotificationColor.None]: [],
        };
        for (const room of rooms) {
            const category = this.getRoomCategory(room);
            map[category].push(room);
        }
        return map;
    }

    // noinspection JSMethodCanBeStatic
    private getRoomCategory(room: Room): NotificationColor {
        // It's fine for us to call this a lot because it's cached, and we shouldn't be
        // wasting anything by doing so as the store holds single references
        const state = RoomNotificationStateStore.instance.getRoomState(room, this.tagId);
        return state.color;
    }

    public async setRooms(rooms: Room[]): Promise<any> {
        if (this.sortingAlgorithm === SortAlgorithm.Manual) {
            this.cachedOrderedRooms = await sortRoomsWithAlgorithm(rooms, this.tagId, this.sortingAlgorithm);
        } else {
            // Every other sorting type affects the categories, not the whole tag.
            const categorized = this.categorizeRooms(rooms);
            for (const category of Object.keys(categorized)) {
                const roomsToOrder = categorized[category];
                categorized[category] = await sortRoomsWithAlgorithm(roomsToOrder, this.tagId, this.sortingAlgorithm);
            }

            const newlyOrganized: Room[] = [];
            const newIndices: ICategoryIndex = {};

            for (const category of CATEGORY_ORDER) {
                newIndices[category] = newlyOrganized.length;
                newlyOrganized.push(...categorized[category]);
            }

            this.indices = newIndices;
            this.cachedOrderedRooms = newlyOrganized;
        }
    }

    private async handleSplice(room: Room, cause: RoomUpdateCause): Promise<boolean> {
        if (cause === RoomUpdateCause.NewRoom) {
            const category = this.getRoomCategory(room);
            this.alterCategoryPositionBy(category, 1, this.indices);
            this.cachedOrderedRooms.splice(this.indices[category], 0, room); // splice in the new room (pre-adjusted)
        } else if (cause === RoomUpdateCause.RoomRemoved) {
            const roomIdx = this.getRoomIndex(room);
            if (roomIdx === -1) {
                console.warn(`Tried to remove unknown room from ${this.tagId}: ${room.roomId}`);
                return false; // no change
            }
            const oldCategory = this.getCategoryFromIndices(roomIdx, this.indices);
            this.alterCategoryPositionBy(oldCategory, -1, this.indices);
            this.cachedOrderedRooms.splice(roomIdx, 1); // remove the room
        } else {
            throw new Error(`Unhandled splice: ${cause}`);
        }
    }

    public async handleRoomUpdate(room: Room, cause: RoomUpdateCause): Promise<boolean> {
        try {
            await this.updateLock.acquireAsync();

            if (cause === RoomUpdateCause.NewRoom || cause === RoomUpdateCause.RoomRemoved) {
                return this.handleSplice(room, cause);
            }

            if (cause !== RoomUpdateCause.Timeline && cause !== RoomUpdateCause.ReadReceipt) {
                throw new Error(`Unsupported update cause: ${cause}`);
            }<commit_msg>Ensure incremental updates to the ImportanceAlgorithm trigger A-Z order<commit_after>/*
Copyright 2018, 2019 New Vector Ltd
Copyright 2020 The Matrix.org Foundation C.I.C.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

import { Room } from "matrix-js-sdk/src/models/room";
import { RoomUpdateCause, TagID } from "../../models";
import { SortAlgorithm } from "../models";
import { sortRoomsWithAlgorithm } from "../tag-sorting";
import { OrderingAlgorithm } from "./OrderingAlgorithm";
import { NotificationColor } from "../../../notifications/NotificationColor";
import { RoomNotificationStateStore } from "../../../notifications/RoomNotificationStateStore";

interface ICategorizedRoomMap {
    // @ts-ignore - TS wants this to be a string, but we know better
    [category: NotificationColor]: Room[];
}

interface ICategoryIndex {
    // @ts-ignore - TS wants this to be a string, but we know better
    [category: NotificationColor]: number; // integer
}

// Caution: changing this means you'll need to update a bunch of assumptions and
// comments! Check the usage of Category carefully to figure out what needs changing
// if you're going to change this array's order.
const CATEGORY_ORDER = [
    NotificationColor.Red,
    NotificationColor.Grey,
    NotificationColor.Bold,
    NotificationColor.None, // idle
];

/**
 * An implementation of the "importance" algorithm for room list sorting. Where
 * the tag sorting algorithm does not interfere, rooms will be ordered into
 * categories of varying importance to the user. Alphabetical sorting does not
 * interfere with this algorithm, however manual ordering does.
 *
 * The importance of a room is defined by the kind of notifications, if any, are
 * present on the room. These are classified internally as Red, Grey, Bold, and
 * Idle. Red rooms have mentions, grey have unread messages, bold is a less noisy
 * version of grey, and idle means all activity has been seen by the user.
 *
 * The algorithm works by monitoring all room changes, including new messages in
 * tracked rooms, to determine if it needs a new category or different placement
 * within the same category. For more information, see the comments contained
 * within the class.
 */
export class ImportanceAlgorithm extends OrderingAlgorithm {
    // This tracks the category for the tag it represents by tracking the index of
    // each category within the list, where zero is the top of the list. This then
    // tracks when rooms change categories and splices the orderedRooms array as
    // needed, preventing many ordering operations.

    private indices: ICategoryIndex = {};

    public constructor(tagId: TagID, initialSortingAlgorithm: SortAlgorithm) {
        super(tagId, initialSortingAlgorithm);
    }

    // noinspection JSMethodCanBeStatic
    private categorizeRooms(rooms: Room[]): ICategorizedRoomMap {
        const map: ICategorizedRoomMap = {
            [NotificationColor.Red]: [],
            [NotificationColor.Grey]: [],
            [NotificationColor.Bold]: [],
            [NotificationColor.None]: [],
        };
        for (const room of rooms) {
            const category = this.getRoomCategory(room);
            map[category].push(room);
        }
        return map;
    }

    // noinspection JSMethodCanBeStatic
    private getRoomCategory(room: Room): NotificationColor {
        // It's fine for us to call this a lot because it's cached, and we shouldn't be
        // wasting anything by doing so as the store holds single references
        const state = RoomNotificationStateStore.instance.getRoomState(room, this.tagId);
        return state.color;
    }

    public async setRooms(rooms: Room[]): Promise<any> {
        if (this.sortingAlgorithm === SortAlgorithm.Manual) {
            this.cachedOrderedRooms = await sortRoomsWithAlgorithm(rooms, this.tagId, this.sortingAlgorithm);
        } else {
            // Every other sorting type affects the categories, not the whole tag.
            const categorized = this.categorizeRooms(rooms);
            for (const category of Object.keys(categorized)) {
                const roomsToOrder = categorized[category];
                categorized[category] = await sortRoomsWithAlgorithm(roomsToOrder, this.tagId, this.sortingAlgorithm);
            }

            const newlyOrganized: Room[] = [];
            const newIndices: ICategoryIndex = {};

            for (const category of CATEGORY_ORDER) {
                newIndices[category] = newlyOrganized.length;
                newlyOrganized.push(...categorized[category]);
            }

            this.indices = newIndices;
            this.cachedOrderedRooms = newlyOrganized;
        }
    }

    private async handleSplice(room: Room, cause: RoomUpdateCause): Promise<boolean> {
        if (cause === RoomUpdateCause.NewRoom) {
            const category = this.getRoomCategory(room);
            this.alterCategoryPositionBy(category, 1, this.indices);
            this.cachedOrderedRooms.splice(this.indices[category], 0, room); // splice in the new room (pre-adjusted)
            await this.sortCategory(category);
        } else if (cause === RoomUpdateCause.RoomRemoved) {
            const roomIdx = this.getRoomIndex(room);
            if (roomIdx === -1) {
                console.warn(`Tried to remove unknown room from ${this.tagId}: ${room.roomId}`);
                return false; // no change
            }
            const oldCategory = this.getCategoryFromIndices(roomIdx, this.indices);
            this.alterCategoryPositionBy(oldCategory, -1, this.indices);
            this.cachedOrderedRooms.splice(roomIdx, 1); // remove the room
        } else {
            throw new Error(`Unhandled splice: ${cause}`);
        }
    }

    public async handleRoomUpdate(room: Room, cause: RoomUpdateCause): Promise<boolean> {
        try {
            await this.updateLock.acquireAsync();

            if (cause === RoomUpdateCause.NewRoom || cause === RoomUpdateCause.RoomRemoved) {
                return this.handleSplice(room, cause);
            }

            if (cause !== RoomUpdateCause.Timeline && cause !== RoomUpdateCause.ReadReceipt) {
                throw new Error(`Unsupported update cause: ${cause}`);
            }
{'fraction_non_alphanumeric': 0.0896137982752156, 'fraction_numerical': 0.0031496062992125984, 'mean_word_length': 2.875617553036908, 'pattern_counts': {'":': 0, '<': 9, '<?xml version=': 0, '>': 9, 'https://': 0, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '7321803', 'n_tokens_mistral': 3461, 'n_tokens_neox': 3312, 'n_words': 1228}