From da46b69d48f9c2960fa4e2ad041ee4b1a8cb7e09 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Sat, 17 Mar 2018 22:07:10 -0600 Subject: [PATCH] Add an early structure for notification processing The general idea is that the room service will call into the notification service for each renderable event. This offloads push rule calculations to the notification service. Notifications and unread counts are both going to be calculated client side. Therefore we can ignore the server values in the sync response. This is because issues like [https://github.com/vector-im/riot-web/issues/3363] are inevitable if we try and do a mix of server and client processing. However, to get to that point we need read receipts so we can see what we've read and what we haven't read. Ideally this reliance on read receipts will disappear when this is resolved: https://github.com/vector-im/riot-meta/issues/66 The amount of interaction between the room service and notification service is still relatively undecided. It will call into the notification service for determining if something should notify, however the read receipt parsing needs to somehow update the notification count somewhere. --- src/app/app.module.ts | 2 + .../matrix/events/account/m.push_rules.ts | 96 ++++++++++++++++++- src/app/services/event-tile.service.ts | 2 +- .../services/matrix/notifications.service.ts | 73 ++++++++++++++ .../views/logged-in/logged-in.component.ts | 4 +- 5 files changed, 172 insertions(+), 5 deletions(-) create mode 100644 src/app/services/matrix/notifications.service.ts diff --git a/src/app/app.module.ts b/src/app/app.module.ts index fe102db..d5cb640 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -72,6 +72,7 @@ import showdown = require("showdown"); import { RoomNameEventTileComponent } from "./elements/event-tiles/state/name/name.component"; import { RoomTopicEventTileComponent } from "./elements/event-tiles/state/topic/topic.component"; import { EventTileService } from "./services/event-tile.service"; +import { NotificationsService } from "./services/matrix/notifications.service"; const DEFAULT_PERFECT_SCROLLBAR_CONFIG: PerfectScrollbarConfigInterface = {}; const SHOWDOWN_CONVERTER = new showdown.Converter(); @@ -137,6 +138,7 @@ const SHOWDOWN_CONVERTER = new showdown.Converter(); SyncService, CommandService, EventTileService, + NotificationsService, // Vendor ], diff --git a/src/app/models/matrix/events/account/m.push_rules.ts b/src/app/models/matrix/events/account/m.push_rules.ts index 556f960..386b35a 100644 --- a/src/app/models/matrix/events/account/m.push_rules.ts +++ b/src/app/models/matrix/events/account/m.push_rules.ts @@ -24,8 +24,98 @@ export interface PushRulesEvent extends AccountDataEvent { } export interface PushRulesEventContent { - // TODO: Determine + device: any; // TODO: Determine - device: any; - global: any; + global: { + content: ContentPushRule[]; + override: OverridePushRule[]; + sender: SenderPushRule[]; // TODO: Determine + room: RoomPushRule[]; + underride: UnderridePushRule[]; + + // Order: + // override - user overrides + // content - unencrypted events, pattern is matched against ?? + // room - all messages in a room. rule_id is always a room ID + // sender - all messages from a user ID. rule_id is always a user ID + // underride - same as override, but low priority + + // Server default rules (.m*) operate at a lower priority than any other kind + + // Within a kind (property on this object), only execute the first matched rule + }; +} + +export interface PushRule { + enabled: boolean; + default: boolean; // as in "server-default" or "predefined" + kind: "content" | "override" | "room" | "sender" | "underride"; + actions: (PushRuleTweak | "dont_notify" | "notify" | "coalesce")[]; // Treat 'coalesce' as 'notify' for now + rule_id: string + // Predefined IDs ("server-default"): + | ".m.rule.contains_user_name" | ".m.rule.master" | ".m.rule.suppress_notices" | ".m.rule.invite_for_me" + | ".m.rule.member_event" | ".m.rule.contains_display_name" | ".m.rule.roomnotif" | ".m.rule.call" + | ".m.rule.room_one_to_one" | ".m.rule.encrypted_room_one_to_one" | ".m.rule.message" | ".m.rule.encrypted"; +} + +export interface ContentPushRule extends PushRule { + kind: "content"; + pattern: string; // glob +} + +export interface OverridePushRule extends PushRule { + kind: "override"; + conditions: PushRuleCondition[]; // none = always matches +} + +export interface RoomPushRule extends PushRule { + kind: "room"; +} + +export interface SenderPushRule extends PushRule { + kind: "sender"; +} + +export interface UnderridePushRule extends PushRule { + kind: "underride"; + conditions: PushRuleCondition[]; // none = always matches +} + +export interface PushRuleTweak { + set_tweak: string; + value?: any; +} + +export interface SoundTweak extends PushRuleTweak { + set_tweak: "sound"; + value: "default" | "ring" | string; // 'default' == make noise +} + +export interface HighlightTweak extends PushRuleTweak { + set_tweak: "highlight"; + value?: boolean; // missing == true +} + +export interface PushRuleCondition { + // Type of check + kind: "event_match" | "sender_notification_permission" | "room_member_count" |"contains_display_name"; +} + +export interface EventMatchCondition extends PushRuleCondition { + kind: "event_match"; + key: string; // dot-separated field to check + pattern: string; // glob - no glob chars means "contains" +} + +export interface RoomMemberCountCondition extends PushRuleCondition { + kind: "room_member_count"; + is: string; // "2", "<2", ">2", "==2", "<=2", ">=2" - no symbol implies == +} + +export interface ContainsDisplayNameCondition extends PushRuleCondition { + kind: "contains_display_name"; +} + +export interface SenderNotificationPermissionCondition extends PushRuleCondition { + kind: "sender_notification_permission"; } \ No newline at end of file diff --git a/src/app/services/event-tile.service.ts b/src/app/services/event-tile.service.ts index f5aaf83..dc281ab 100644 --- a/src/app/services/event-tile.service.ts +++ b/src/app/services/event-tile.service.ts @@ -68,7 +68,7 @@ export class EventTileService { * @param {RoomEvent} event The event to check * @returns {boolean} True if the event is renderable, false otherwise */ - public isRenderable(event: RoomEvent): boolean{ + public isRenderable(event: RoomEvent): boolean { return !!this.tileMap[event.type]; } } \ No newline at end of file diff --git a/src/app/services/matrix/notifications.service.ts b/src/app/services/matrix/notifications.service.ts new file mode 100644 index 0000000..cd18efc --- /dev/null +++ b/src/app/services/matrix/notifications.service.ts @@ -0,0 +1,73 @@ +/* + * Evelium - A matrix client + * Copyright (C) 2018 Travis Ralston + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +import { AuthenticatedApi } from "./authenticated-api"; +import { HttpClient } from "@angular/common/http"; +import { AuthService } from "./auth.service"; +import { Injectable } from "@angular/core"; +import { AccountService } from "./account.service"; +import { AccountDataEvent } from "../../models/matrix/events/account/account-data-event"; +import { PushRulesEvent } from "../../models/matrix/events/account/m.push_rules"; + +// For singleton access +let notificationsHandler: NotificationsHandler; + +// .m.rule.master - enabled=false means enabled +// .m.rule.suppress_notices - should be an override +// + +@Injectable() +export class NotificationsService extends AuthenticatedApi { + + constructor(http: HttpClient, auth: AuthService, private account: AccountService) { + super(http, auth); + } + + private checkHandler(): void { + if (!notificationsHandler) notificationsHandler = new NotificationsHandler(this.account); + } + + public getPushRules(): any { + this.checkHandler(); + return notificationsHandler.getPushRules(); + } +} + +class NotificationsHandler { + + constructor(private account: AccountService) { + + account.accountData.events.subscribe(event => this.processPushRules(event)); + + account.accountData.get("m.push_rules") + .then(e => this.processPushRules(e)).catch(() => Promise.resolve()); // swallow errors + } + + private processPushRules(evt: AccountDataEvent) { + if (evt.type !== "m.push_rules") return; + const rules = evt; + + console.log(rules); + } + + public async getPushRules() { + const rules = await this.account.accountData.get("m.push_rules"); + console.log(rules); + return rules; + } +} \ No newline at end of file diff --git a/src/app/views/logged-in/logged-in.component.ts b/src/app/views/logged-in/logged-in.component.ts index 1885c0a..a729462 100644 --- a/src/app/views/logged-in/logged-in.component.ts +++ b/src/app/views/logged-in/logged-in.component.ts @@ -19,6 +19,7 @@ import { Component, OnInit } from "@angular/core"; import { SyncService } from "../../services/matrix/sync.service"; import { RoomService } from "../../services/matrix/room.service"; +import { NotificationsService } from "../../services/matrix/notifications.service"; @Component({ templateUrl: "./logged-in.component.html", @@ -28,7 +29,8 @@ export class LoggedInComponent implements OnInit { public receivedRoomList = false; - constructor(private sync: SyncService, private rooms: RoomService) { + constructor(private sync: SyncService, private rooms: RoomService, notifications: NotificationsService) { + notifications.getPushRules(); } public ngOnInit() {