mirror of
https://github.com/ilyakooo0/urbit.git
synced 2024-12-14 17:41:33 +03:00
Merge pull request #3892 from urbit/lf/hark-chat-hook
hark-chat-hook: Notifications
This commit is contained in:
commit
f31dd0c047
@ -5,7 +5,7 @@
|
||||
/- glob
|
||||
/+ default-agent, verb, dbug
|
||||
|%
|
||||
++ hash 0v4.cf9m1.t0ofg.dtig4.av3jh.2f2db
|
||||
++ hash 0v6.9vk2h.hr87m.nn63p.8kmo5.k4ljt
|
||||
+$ state-0 [%0 hash=@uv glob=(unit (each glob:glob tid=@ta))]
|
||||
+$ all-states
|
||||
$% state-0
|
||||
|
184
pkg/arvo/app/hark-chat-hook.hoon
Normal file
184
pkg/arvo/app/hark-chat-hook.hoon
Normal file
@ -0,0 +1,184 @@
|
||||
:: hark-chat-hook: notifications for chat-store [landscape]
|
||||
::
|
||||
/- store=hark-store, post, group-store, metadata-store, hook=hark-chat-hook
|
||||
/+ resource, metadata, default-agent, dbug, chat-store
|
||||
::
|
||||
~% %hark-chat-hook-top ..is ~
|
||||
|%
|
||||
+$ card card:agent:gall
|
||||
+$ versioned-state
|
||||
$% state-0
|
||||
==
|
||||
::
|
||||
+$ state-0
|
||||
$: %0
|
||||
watching=(set path)
|
||||
mentions=_&
|
||||
==
|
||||
::
|
||||
--
|
||||
::
|
||||
=| state-0
|
||||
=* state -
|
||||
::
|
||||
=<
|
||||
%- agent:dbug
|
||||
^- agent:gall
|
||||
~% %hark-chat-hook-agent ..card ~
|
||||
|_ =bowl:gall
|
||||
+* this .
|
||||
ha ~(. +> bowl)
|
||||
def ~(. (default-agent this %|) bowl)
|
||||
met ~(. metadata bowl)
|
||||
::
|
||||
++ on-init
|
||||
:_ this
|
||||
~[watch-chat:ha]
|
||||
::
|
||||
++ on-save !>(state)
|
||||
++ on-load
|
||||
|= old=vase
|
||||
^- (quip card _this)
|
||||
`this(state !<(state-0 old))
|
||||
::
|
||||
++ on-watch
|
||||
|= =path
|
||||
^- (quip card _this)
|
||||
=^ cards state
|
||||
?+ path (on-watch:def path)
|
||||
::
|
||||
[%updates ~]
|
||||
:_ state
|
||||
%+ give:ha ~
|
||||
:* %initial
|
||||
watching
|
||||
==
|
||||
==
|
||||
[cards this]
|
||||
::
|
||||
++ on-poke
|
||||
~/ %hark-chat-hook-poke
|
||||
|= [=mark =vase]
|
||||
^- (quip card _this)
|
||||
|^
|
||||
?> (team:title our.bowl src.bowl)
|
||||
=^ cards state
|
||||
?+ mark (on-poke:def mark vase)
|
||||
%hark-chat-hook-action
|
||||
(hark-chat-hook-action !<(action:hook vase))
|
||||
==
|
||||
[cards this]
|
||||
::
|
||||
++ hark-chat-hook-action
|
||||
|= =action:hook
|
||||
^- (quip card _state)
|
||||
|^
|
||||
?- -.action
|
||||
%listen (listen +.action)
|
||||
%ignore (ignore +.action)
|
||||
%set-mentions (set-mentions +.action)
|
||||
==
|
||||
++ listen
|
||||
|= chat=path
|
||||
^- (quip card _state)
|
||||
:- (give:ha ~[/updates] [%listen chat])
|
||||
state(watching (~(put in watching) chat))
|
||||
::
|
||||
++ ignore
|
||||
|= chat=path
|
||||
^- (quip card _state)
|
||||
:- (give:ha ~[/updates] [%ignore chat])
|
||||
state(watching (~(del in watching) chat))
|
||||
::
|
||||
++ set-mentions
|
||||
|= ment=?
|
||||
^- (quip card _state)
|
||||
:- (give:ha ~[/updates] [%set-mentions ment])
|
||||
state(mentions ment)
|
||||
--
|
||||
--
|
||||
::
|
||||
++ on-agent
|
||||
~/ %hark-chat-hook-agent
|
||||
|= [=wire =sign:agent:gall]
|
||||
^- (quip card _this)
|
||||
|^
|
||||
?+ -.sign (on-agent:def wire sign)
|
||||
%kick
|
||||
:_ this
|
||||
?. ?=([%chat ~] wire)
|
||||
~
|
||||
~[watch-chat:ha]
|
||||
::
|
||||
%fact
|
||||
?. ?=(%chat-update p.cage.sign)
|
||||
(on-agent:def wire sign)
|
||||
=^ cards state
|
||||
(chat-update !<(update:chat-store q.cage.sign))
|
||||
[cards this]
|
||||
==
|
||||
::
|
||||
++ chat-update
|
||||
|= =update:chat-store
|
||||
^- (quip card _state)
|
||||
:_ state
|
||||
?+ -.update ~
|
||||
%message (process-envelope path.update envelope.update)
|
||||
::
|
||||
%messages
|
||||
%- zing
|
||||
(turn envelopes.update (cury process-envelope path.update))
|
||||
==
|
||||
++ is-mention
|
||||
|= [=path =envelope:chat-store]
|
||||
?. ?=(%text -.letter.envelope) %.n
|
||||
?& mentions
|
||||
?= ^
|
||||
(find (scow %p our.bowl) (trip text.letter.envelope))
|
||||
==
|
||||
::
|
||||
++ is-notification
|
||||
|= [=path =envelope:chat-store]
|
||||
?& (~(has in watching) path)
|
||||
!=(author.envelope our.bowl)
|
||||
==
|
||||
::
|
||||
++ process-envelope
|
||||
|= [=path =envelope:chat-store]
|
||||
^- (list card)
|
||||
=/ mention=?
|
||||
(is-mention path envelope)
|
||||
?. ?|(mention (is-notification path envelope))
|
||||
~
|
||||
=/ =index:store
|
||||
[%chat path mention]
|
||||
=/ =contents:store
|
||||
[%chat ~[envelope]]
|
||||
~[(poke-store %add index when.envelope %.n contents)]
|
||||
::
|
||||
++ poke-store
|
||||
|= =action:store
|
||||
^- card
|
||||
=/ =cage
|
||||
hark-action+!>(action)
|
||||
[%pass /store %agent [our.bowl %hark-store] %poke cage]
|
||||
--
|
||||
::
|
||||
++ on-peek on-peek:def
|
||||
::
|
||||
++ on-leave on-leave:def
|
||||
++ on-arvo on-arvo:def
|
||||
++ on-fail on-fail:def
|
||||
--
|
||||
|_ =bowl:gall
|
||||
::
|
||||
::
|
||||
++ give
|
||||
|= [paths=(list path) =update:hook]
|
||||
^- (list card)
|
||||
[%give %fact paths hark-chat-hook-update+!>(update)]~
|
||||
::
|
||||
++ watch-chat
|
||||
^- card
|
||||
[%pass /chat %agent [our.bowl %chat-store] %watch /updates]
|
||||
--
|
@ -236,6 +236,10 @@
|
||||
|= [existing=notification:store new=notification:store]
|
||||
^- notification:store
|
||||
?- -.contents.existing
|
||||
::
|
||||
%chat
|
||||
?> ?=(%chat -.contents.new)
|
||||
existing(list.contents (weld list.contents.existing list.contents.new))
|
||||
::
|
||||
%graph
|
||||
?> ?=(%graph -.contents.new)
|
||||
|
@ -24,6 +24,6 @@
|
||||
<div id="portal-root"></div>
|
||||
<script src="/~landscape/js/channel.js"></script>
|
||||
<script src="/~landscape/js/session.js"></script>
|
||||
<script src="/~landscape/js/bundle/index.20c57e72701ff6afa573.js"></script>
|
||||
<script src="/~landscape/js/bundle/index.5f9890df6be59a4ff9c5.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
33
pkg/arvo/lib/hark/chat-hook.hoon
Normal file
33
pkg/arvo/lib/hark/chat-hook.hoon
Normal file
@ -0,0 +1,33 @@
|
||||
/- sur=hark-chat-hook
|
||||
^?
|
||||
=< [. sur]
|
||||
=, sur
|
||||
|%
|
||||
::
|
||||
++ dejs
|
||||
=, dejs:format
|
||||
|%
|
||||
::
|
||||
++ action
|
||||
%- of
|
||||
:~ listen+pa
|
||||
ignore+pa
|
||||
set-mentions+bo
|
||||
==
|
||||
--
|
||||
::
|
||||
++ enjs
|
||||
=, enjs:format
|
||||
|%
|
||||
::
|
||||
++ update
|
||||
|= upd=^update
|
||||
%+ frond -.upd
|
||||
?- -.upd
|
||||
?(%listen %ignore) (path chat.upd)
|
||||
%set-mentions b+mentions.upd
|
||||
%initial a+(turn ~(tap in watching.upd) path)
|
||||
==
|
||||
--
|
||||
--
|
||||
|
@ -1,5 +1,5 @@
|
||||
/- sur=hark-store, post
|
||||
/+ resource, graph-store, group-store
|
||||
/+ resource, graph-store, group-store, chat-store
|
||||
^?
|
||||
=< [. sur]
|
||||
=, sur
|
||||
@ -11,6 +11,13 @@
|
||||
%- of
|
||||
:~ graph+graph-index
|
||||
group+group-index
|
||||
chat+chat-index
|
||||
==
|
||||
::
|
||||
++ chat-index
|
||||
%- ot
|
||||
:~ chat+pa
|
||||
mention+bo
|
||||
==
|
||||
::
|
||||
++ group-index
|
||||
@ -106,7 +113,16 @@
|
||||
?- -.index
|
||||
%graph (graph-index +.index)
|
||||
%group (group-index +.index)
|
||||
%chat (chat-index +.index)
|
||||
==
|
||||
::
|
||||
++ chat-index
|
||||
|= [chat=^path mention=?]
|
||||
^- json
|
||||
%- pairs
|
||||
:~ chat+(path chat)
|
||||
mention+b+mention
|
||||
==
|
||||
::
|
||||
++ graph-index
|
||||
|= [group=resource graph=resource module=@t description=@t]
|
||||
@ -144,8 +160,15 @@
|
||||
?- -.contents
|
||||
%graph (graph-contents +.contents)
|
||||
%group (group-contents +.contents)
|
||||
%chat (chat-contents +.contents)
|
||||
==
|
||||
::
|
||||
++ chat-contents
|
||||
|= =(list envelope:chat-store)
|
||||
^- json
|
||||
:- %a
|
||||
(turn list envelope:enjs:chat-store)
|
||||
::
|
||||
++ graph-contents
|
||||
|= =(list post:post)
|
||||
^- json
|
||||
|
13
pkg/arvo/mar/hark/chat-hook-action.hoon
Normal file
13
pkg/arvo/mar/hark/chat-hook-action.hoon
Normal file
@ -0,0 +1,13 @@
|
||||
/+ *hark-chat-hook
|
||||
|_ act=action
|
||||
++ grad %noun
|
||||
++ grow
|
||||
|%
|
||||
++ noun act
|
||||
--
|
||||
++ grab
|
||||
|%
|
||||
++ noun action
|
||||
++ json action:dejs
|
||||
--
|
||||
--
|
16
pkg/arvo/mar/hark/chat-hook-update.hoon
Normal file
16
pkg/arvo/mar/hark/chat-hook-update.hoon
Normal file
@ -0,0 +1,16 @@
|
||||
/+ *hark-chat-hook
|
||||
|_ upd=update
|
||||
++ grad %noun
|
||||
++ grow
|
||||
|%
|
||||
++ noun upd
|
||||
++ json
|
||||
%+ frond:enjs:format
|
||||
%hark-chat-hook-update
|
||||
(update:enjs upd)
|
||||
--
|
||||
++ grab
|
||||
|%
|
||||
++ noun update
|
||||
--
|
||||
--
|
16
pkg/arvo/sur/hark-chat-hook.hoon
Normal file
16
pkg/arvo/sur/hark-chat-hook.hoon
Normal file
@ -0,0 +1,16 @@
|
||||
^?
|
||||
|%
|
||||
::
|
||||
+$ action
|
||||
$% [?(%listen %ignore) chat=path]
|
||||
[%set-mentions mentions=?]
|
||||
==
|
||||
::
|
||||
+$ update
|
||||
$%
|
||||
action
|
||||
$: %initial
|
||||
watching=(set path)
|
||||
==
|
||||
==
|
||||
--
|
@ -1,10 +1,11 @@
|
||||
/- *resource, graph-store, post, group-store, metadata-store
|
||||
/- *resource, graph-store, post, group-store, metadata-store, chat-store
|
||||
^?
|
||||
|%
|
||||
::
|
||||
+$ index
|
||||
$% [%graph group=resource graph=resource module=@t description=@t]
|
||||
[%group group=resource description=@t]
|
||||
[%chat chat=path mention=?]
|
||||
==
|
||||
::
|
||||
+$ group-contents
|
||||
@ -19,6 +20,7 @@
|
||||
+$ contents
|
||||
$% [%graph =(list post:post)]
|
||||
[%group =(list group-contents)]
|
||||
[%chat =(list envelope:chat-store)]
|
||||
==
|
||||
::
|
||||
+$ timebox
|
||||
|
@ -2,6 +2,7 @@ import BaseApi from "./base";
|
||||
import { StoreState } from "../store/type";
|
||||
import { dateToDa, decToUd } from "../lib/util";
|
||||
import {NotifIndex} from "~/types";
|
||||
import { BigInteger } from 'big-integer';
|
||||
|
||||
export class HarkApi extends BaseApi<StoreState> {
|
||||
private harkAction(action: any): Promise<any> {
|
||||
@ -15,6 +16,10 @@ export class HarkApi extends BaseApi<StoreState> {
|
||||
private groupHookAction(action: any) {
|
||||
return this.action("hark-group-hook", "hark-group-hook-action", action);
|
||||
}
|
||||
|
||||
private chatHookAction(action: any) {
|
||||
return this.action("hark-chat-hook", "hark-chat-hook-action", action);
|
||||
}
|
||||
|
||||
private actOnNotification(frond: string, intTime: BigInteger, index: NotifIndex) {
|
||||
const time = decToUd(intTime.toString());
|
||||
@ -26,12 +31,11 @@ export class HarkApi extends BaseApi<StoreState> {
|
||||
});
|
||||
}
|
||||
|
||||
private graphHookAction(action: any) {
|
||||
return this.action("hark-graph-hook", "hark-graph-hook-action", action);
|
||||
}
|
||||
|
||||
setMentions(mentions: boolean) {
|
||||
return this.graphHookAction({
|
||||
async setMentions(mentions: boolean) {
|
||||
await this.graphHookAction({
|
||||
'set-mentions': mentions
|
||||
});
|
||||
return this.chatHookAction({
|
||||
'set-mentions': mentions
|
||||
});
|
||||
}
|
||||
@ -73,6 +77,9 @@ export class HarkApi extends BaseApi<StoreState> {
|
||||
const { group } = index.group;
|
||||
return this.ignoreGroup(group);
|
||||
}
|
||||
if('chat' in index) {
|
||||
return this.ignoreChat(index.chat);
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
@ -83,6 +90,9 @@ export class HarkApi extends BaseApi<StoreState> {
|
||||
if('group' in index) {
|
||||
return this.listenGroup(index.group.group);
|
||||
}
|
||||
if('chat' in index) {
|
||||
return this.listenChat(index.chat);
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
@ -98,6 +108,12 @@ export class HarkApi extends BaseApi<StoreState> {
|
||||
})
|
||||
}
|
||||
|
||||
ignoreChat(chat: string) {
|
||||
return this.chatHookAction({
|
||||
ignore: chat
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
listenGroup(group: string) {
|
||||
return this.groupHookAction({
|
||||
@ -111,6 +127,12 @@ export class HarkApi extends BaseApi<StoreState> {
|
||||
})
|
||||
}
|
||||
|
||||
listenChat(chat: string) {
|
||||
return this.chatHookAction({
|
||||
listen: chat
|
||||
});
|
||||
}
|
||||
|
||||
async getTimeSubset(start?: Date, end?: Date) {
|
||||
const s = start ? dateToDa(start) : "-";
|
||||
const e = end ? dateToDa(end) : "-";
|
||||
|
@ -1,6 +1,5 @@
|
||||
import {
|
||||
Notifications,
|
||||
Notification,
|
||||
NotifIndex,
|
||||
NotificationGraphConfig,
|
||||
GroupNotificationsConfig,
|
||||
@ -14,6 +13,7 @@ type HarkState = {
|
||||
notificationsCount: number;
|
||||
notificationsGraphConfig: NotificationGraphConfig;
|
||||
notificationsGroupConfig: GroupNotificationsConfig;
|
||||
notificationsChatConfig: string[];
|
||||
};
|
||||
|
||||
export const HarkReducer = (json: any, state: HarkState) => {
|
||||
@ -36,8 +36,39 @@ export const HarkReducer = (json: any, state: HarkState) => {
|
||||
groupListen(groupHookData, state);
|
||||
groupIgnore(groupHookData, state);
|
||||
}
|
||||
|
||||
const chatHookData = _.get(json, "hark-chat-hook-update", false);
|
||||
if(chatHookData) {
|
||||
|
||||
chatInitial(chatHookData, state);
|
||||
chatListen(chatHookData, state);
|
||||
chatIgnore(chatHookData, state);
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
function chatInitial(json: any, state: HarkState) {
|
||||
const data = _.get(json, "initial", false);
|
||||
if (data) {
|
||||
state.notificationsChatConfig = data;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function chatListen(json: any, state: HarkState) {
|
||||
const data = _.get(json, "listen", false);
|
||||
if (data) {
|
||||
state.notificationsChatConfig = [...state.notificationsChatConfig, data];
|
||||
}
|
||||
}
|
||||
|
||||
function chatIgnore(json: any, state: HarkState) {
|
||||
const data = _.get(json, "ignore", false);
|
||||
if (data) {
|
||||
state.notificationsChatConfig = state.notificationsChatConfig.filter(x => x !== data);
|
||||
}
|
||||
}
|
||||
|
||||
function groupInitial(json: any, state: HarkState) {
|
||||
const data = _.get(json, "initial", false);
|
||||
if (data) {
|
||||
@ -177,6 +208,9 @@ function notifIdxEqual(a: NotifIndex, b: NotifIndex) {
|
||||
a.group.group === b.group.group &&
|
||||
a.group.description === b.group.description
|
||||
);
|
||||
} else if ("chat" in a && "chat" in b) {
|
||||
return a.chat.chat === b.chat.chat &&
|
||||
a.chat.mention === b.chat.mention;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
@ -100,6 +100,7 @@ export default class GlobalStore extends BaseStore<StoreState> {
|
||||
notifications: new BigIntOrderedMap<Timebox>(),
|
||||
archivedNotifications: new BigIntOrderedMap<Timebox>(),
|
||||
notificationsGroupConfig: [],
|
||||
notificationsChatConfig: [],
|
||||
notificationsGraphConfig: {
|
||||
watchOnSelf: false,
|
||||
mentions: false,
|
||||
|
@ -62,6 +62,7 @@ export interface StoreState {
|
||||
notifications: Notifications;
|
||||
notificationsGraphConfig: NotificationGraphConfig;
|
||||
notificationsGroupConfig: GroupNotificationsConfig;
|
||||
notificationsChatConfig: string[];
|
||||
notificationsCount: number,
|
||||
doNotDisturb: boolean;
|
||||
}
|
||||
|
@ -54,6 +54,7 @@ export default class GlobalSubscription extends BaseSubscription<StoreState> {
|
||||
this.subscribe('/updates', 'hark-store');
|
||||
this.subscribe('/updates', 'hark-graph-hook');
|
||||
this.subscribe('/updates', 'hark-group-hook');
|
||||
this.subscribe('/updates', 'hark-chat-hook');
|
||||
}
|
||||
|
||||
restart() {
|
||||
|
@ -2,6 +2,7 @@ import _ from "lodash";
|
||||
import { Post } from "./graph-update";
|
||||
import { GroupUpdate } from "./group-update";
|
||||
import { BigIntOrderedMap } from "~/logic/lib/BigIntOrderedMap";
|
||||
import { Envelope } from './chat-update';
|
||||
|
||||
type GraphNotifDescription = "link" | "comment";
|
||||
|
||||
@ -17,17 +18,26 @@ export interface GroupNotifIndex {
|
||||
description: string;
|
||||
}
|
||||
|
||||
export interface ChatNotifIndex {
|
||||
chat: string;
|
||||
mention: boolean;
|
||||
}
|
||||
|
||||
export type NotifIndex =
|
||||
| { graph: GraphNotifIndex }
|
||||
| { group: GroupNotifIndex };
|
||||
| { group: GroupNotifIndex }
|
||||
| { chat: ChatNotifIndex };
|
||||
|
||||
export type GraphNotificationContents = Post[];
|
||||
|
||||
export type GroupNotificationContents = GroupUpdate[];
|
||||
|
||||
export type ChatNotificationContents = Envelope[];
|
||||
|
||||
export type NotificationContents =
|
||||
| { graph: GraphNotificationContents }
|
||||
| { group: GroupNotificationContents };
|
||||
| { group: GroupNotificationContents }
|
||||
| { chat: ChatNotificationContents };
|
||||
|
||||
interface Notification {
|
||||
read: boolean;
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { useRef, useCallback } from "react";
|
||||
import React, { useRef, useCallback, useEffect } from "react";
|
||||
import { RouteComponentProps } from "react-router-dom";
|
||||
import { Col } from "@tlon/indigo-react";
|
||||
import _ from 'lodash';
|
||||
@ -94,6 +94,15 @@ export function ChatResource(props: ChatResourceProps) {
|
||||
station,
|
||||
]);
|
||||
|
||||
const scrollTo = new URLSearchParams(location.search).get('msg');
|
||||
useEffect(() => {
|
||||
const clear = () => {
|
||||
props.history.replace(location.pathname);
|
||||
}
|
||||
setTimeout(clear, 10000);
|
||||
return clear;
|
||||
}, [station]);
|
||||
|
||||
return (
|
||||
<Col {...bind} height="100%" overflow="hidden" position="relative">
|
||||
{dragging && <SubmitDragger />}
|
||||
@ -118,6 +127,7 @@ export function ChatResource(props: ChatResourceProps) {
|
||||
hideNicknames={props.hideNicknames}
|
||||
hideAvatars={props.hideAvatars}
|
||||
location={props.location}
|
||||
scrollTo={scrollTo ? parseInt(scrollTo, 10) : undefined}
|
||||
/>
|
||||
<ChatInput
|
||||
ref={chatInput}
|
||||
|
@ -31,8 +31,8 @@ export const DayBreak = ({ when }) => (
|
||||
interface ChatMessageProps {
|
||||
measure(element): void;
|
||||
msg: Envelope | IMessage;
|
||||
previousMsg: Envelope | IMessage | undefined;
|
||||
nextMsg: Envelope | IMessage | undefined;
|
||||
previousMsg?: Envelope | IMessage;
|
||||
nextMsg?: Envelope | IMessage;
|
||||
isLastRead: boolean;
|
||||
group: Group;
|
||||
association: Association;
|
||||
@ -48,6 +48,7 @@ interface ChatMessageProps {
|
||||
unreadMarkerRef: React.RefObject<HTMLDivElement>;
|
||||
history: any;
|
||||
api: any;
|
||||
highlighted?: boolean;
|
||||
}
|
||||
|
||||
export default class ChatMessage extends Component<ChatMessageProps> {
|
||||
@ -84,7 +85,8 @@ export default class ChatMessage extends Component<ChatMessageProps> {
|
||||
isLastMessage,
|
||||
unreadMarkerRef,
|
||||
history,
|
||||
api
|
||||
api,
|
||||
highlighted
|
||||
} = this.props;
|
||||
|
||||
const renderSigil = Boolean((nextMsg && msg.author !== nextMsg.author) || !nextMsg || msg.number === 1);
|
||||
@ -115,7 +117,8 @@ export default class ChatMessage extends Component<ChatMessageProps> {
|
||||
isPending,
|
||||
history,
|
||||
api,
|
||||
scrollWindow
|
||||
scrollWindow,
|
||||
highlighted
|
||||
};
|
||||
|
||||
const unreadContainerStyle = {
|
||||
@ -124,6 +127,7 @@ export default class ChatMessage extends Component<ChatMessageProps> {
|
||||
|
||||
return (
|
||||
<Box
|
||||
bg={highlighted ? 'washedBlue' : 'white'}
|
||||
width='100%'
|
||||
display='flex'
|
||||
flexWrap='wrap'
|
||||
@ -165,6 +169,8 @@ interface MessageProps {
|
||||
};
|
||||
|
||||
export class MessageWithSigil extends PureComponent<MessageProps> {
|
||||
isDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
|
||||
render() {
|
||||
const {
|
||||
msg,
|
||||
@ -176,8 +182,8 @@ export class MessageWithSigil extends PureComponent<MessageProps> {
|
||||
hideAvatars,
|
||||
remoteContentPolicy,
|
||||
measure,
|
||||
history,
|
||||
api,
|
||||
history,
|
||||
scrollWindow
|
||||
} = this.props;
|
||||
|
||||
@ -185,8 +191,8 @@ export class MessageWithSigil extends PureComponent<MessageProps> {
|
||||
const contact = msg.author in contacts ? contacts[msg.author] : false;
|
||||
const showNickname = !hideNicknames && contact && contact.nickname;
|
||||
const name = showNickname ? contact.nickname : cite(msg.author);
|
||||
const color = contact ? `#${uxToHex(contact.color)}` : '#000000';
|
||||
const sigilClass = contact ? '' : 'mix-blend-diff';
|
||||
const color = contact ? `#${uxToHex(contact.color)}` : this.isDark ? '#000000' :'#FFFFFF'
|
||||
const sigilClass = contact ? '' : this.isDark ? 'mix-blend-diff' : 'mix-blend-darken';
|
||||
|
||||
let nameSpan = null;
|
||||
|
||||
@ -213,7 +219,7 @@ export class MessageWithSigil extends PureComponent<MessageProps> {
|
||||
scrollWindow={scrollWindow}
|
||||
history={history}
|
||||
api={api}
|
||||
className="fl pr3 v-top bg-white bg-gray0-d pt1"
|
||||
className="fl pr3 v-top pt1"
|
||||
/>
|
||||
<Box flexGrow='1' display='block' className="clamp-message">
|
||||
<Box
|
||||
@ -239,7 +245,7 @@ export class MessageWithSigil extends PureComponent<MessageProps> {
|
||||
<Text flexShrink='0' gray mono className="v-mid">{timestamp}</Text>
|
||||
<Text gray mono ml={2} className="v-mid child dn-s">{datestamp}</Text>
|
||||
</Box>
|
||||
<Box fontSize='14px'><MessageContent content={msg.letter} remoteContentPolicy={remoteContentPolicy} measure={measure} /></Box>
|
||||
<Box fontSize='14px'><MessageContent content={msg.letter} remoteContentPolicy={remoteContentPolicy} measure={measure} /></Box>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
|
@ -43,6 +43,7 @@ type ChatWindowProps = RouteComponentProps<{
|
||||
hideNicknames: boolean;
|
||||
hideAvatars: boolean;
|
||||
remoteContentPolicy: LocalUpdateRemoteContentPolicy;
|
||||
scrollTo?: number;
|
||||
}
|
||||
|
||||
interface ChatWindowState {
|
||||
@ -84,6 +85,10 @@ export default class ChatWindow extends Component<ChatWindowProps, ChatWindowSta
|
||||
window.addEventListener('focus', this.handleWindowFocus);
|
||||
this.initialFetch();
|
||||
setTimeout(() => {
|
||||
if(this.props.scrollTo) {
|
||||
this.scrollToUnread();
|
||||
}
|
||||
|
||||
this.setState({ initialized: true });
|
||||
}, this.INITIALIZATION_MAX_TIME);
|
||||
}
|
||||
@ -167,8 +172,9 @@ export default class ChatWindow extends Component<ChatWindowProps, ChatWindowSta
|
||||
}
|
||||
|
||||
scrollToUnread() {
|
||||
const { mailboxSize, unreadCount } = this.props;
|
||||
this.virtualList?.scrollToData(mailboxSize - unreadCount);
|
||||
const { mailboxSize, unreadCount, scrollTo } = this.props;
|
||||
const target = scrollTo || (mailboxSize - unreadCount);
|
||||
this.virtualList?.scrollToData(target);
|
||||
}
|
||||
|
||||
dismissUnread() {
|
||||
@ -297,7 +303,8 @@ export default class ChatWindow extends Component<ChatWindowProps, ChatWindowSta
|
||||
const isPending: boolean = 'pending' in msg && Boolean(msg.pending);
|
||||
const isLastMessage: boolean = Boolean(index === lastMessage)
|
||||
const isLastRead: boolean = Boolean(!isLastMessage && index === this.state.lastRead);
|
||||
const props = { measure, scrollWindow, isPending, isLastRead, isLastMessage, msg, ...messageProps };
|
||||
const highlighted = index === this.props.scrollTo;
|
||||
const props = { measure, highlighted, scrollWindow, isPending, isLastRead, isLastMessage, msg, ...messageProps };
|
||||
return (
|
||||
<ChatMessage
|
||||
key={index}
|
||||
|
@ -87,6 +87,10 @@ h2 {
|
||||
mix-blend-mode: difference;
|
||||
}
|
||||
|
||||
.mix-blend-darken {
|
||||
mix-blend-mode: darken;
|
||||
}
|
||||
|
||||
.placeholder-inter::placeholder {
|
||||
font-family: "Inter", sans-serif;
|
||||
}
|
||||
|
99
pkg/interface/src/views/apps/notifications/chat.tsx
Normal file
99
pkg/interface/src/views/apps/notifications/chat.tsx
Normal file
@ -0,0 +1,99 @@
|
||||
import React, { useCallback } from "react";
|
||||
import _ from "lodash";
|
||||
import { Link } from "react-router-dom";
|
||||
import GlobalApi from "~/logic/api/global";
|
||||
import {
|
||||
Rolodex,
|
||||
Associations,
|
||||
ChatNotifIndex,
|
||||
ChatNotificationContents,
|
||||
Groups,
|
||||
} from "~/types";
|
||||
import { BigInteger } from "big-integer";
|
||||
import { Box, Col } from "@tlon/indigo-react";
|
||||
import { Header } from "./header";
|
||||
import { pluralize } from "~/logic/lib/util";
|
||||
import ChatMessage from "../chat/components/ChatMessage";
|
||||
|
||||
function describeNotification(mention: boolean, lent: number) {
|
||||
const msg = pluralize("message", lent !== 1);
|
||||
if (mention) {
|
||||
return `mentioned you in ${msg} in`;
|
||||
}
|
||||
return `sent ${msg} in`;
|
||||
}
|
||||
|
||||
export function ChatNotification(props: {
|
||||
index: ChatNotifIndex;
|
||||
contents: ChatNotificationContents;
|
||||
archived: boolean;
|
||||
read: boolean;
|
||||
time: number;
|
||||
timebox: BigInteger;
|
||||
associations: Associations;
|
||||
contacts: Rolodex;
|
||||
groups: Groups;
|
||||
api: GlobalApi;
|
||||
}) {
|
||||
const { index, contents, read, time, api, timebox } = props;
|
||||
const authors = _.map(contents, "author");
|
||||
|
||||
const { chat, mention } = index;
|
||||
const association = props.associations.chat[chat];
|
||||
const groupPath = association["group-path"];
|
||||
const appPath = association["app-path"];
|
||||
|
||||
const group = props.groups[groupPath];
|
||||
|
||||
const desc = describeNotification(mention, contents.length);
|
||||
const groupContacts = props.contacts[groupPath];
|
||||
|
||||
const onClick = useCallback(() => {
|
||||
if (props.archived) {
|
||||
return;
|
||||
}
|
||||
|
||||
const func = read ? "unread" : "read";
|
||||
return api.hark[func](timebox, { chat: index });
|
||||
}, [api, timebox, index, read]);
|
||||
|
||||
return (
|
||||
<Col onClick={onClick} flexGrow="1" p="2">
|
||||
<Header
|
||||
chat
|
||||
associations={props.associations}
|
||||
read={read}
|
||||
archived={props.archived}
|
||||
time={time}
|
||||
authors={authors}
|
||||
moduleIcon="Chat"
|
||||
channel={chat}
|
||||
contacts={props.contacts}
|
||||
group={groupPath}
|
||||
description={desc}
|
||||
/>
|
||||
<Col pb="3" pl="5">
|
||||
{_.map(_.take(contents, 5), (content, idx) => {
|
||||
const to = `/~landscape${groupPath}/resource/chat${appPath}?msg=${content.number}`;
|
||||
return (
|
||||
<Link key={idx} to={to}>
|
||||
<ChatMessage
|
||||
measure={() => {}}
|
||||
msg={content}
|
||||
isLastRead={false}
|
||||
group={group}
|
||||
contacts={groupContacts}
|
||||
/>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
{contents.length > 5 && (
|
||||
<Box ml="4" mt="3" mb="2" color="gray" fontSize="14px">
|
||||
and {contents.length - 5} other message
|
||||
{contents.length > 6 ? "s" : ""}
|
||||
</Box>
|
||||
)}
|
||||
</Col>
|
||||
</Col>
|
||||
);
|
||||
}
|
@ -42,6 +42,7 @@ export function Header(props: {
|
||||
time: number;
|
||||
read: boolean;
|
||||
associations: Associations;
|
||||
chat?: boolean;
|
||||
}) {
|
||||
const { description, channel, group, moduleIcon, read } = props;
|
||||
const contacts = props.contacts[group] || {};
|
||||
@ -70,8 +71,9 @@ export function Header(props: {
|
||||
const groupTitle =
|
||||
props.associations.contacts?.[props.group]?.metadata?.title || props.group;
|
||||
|
||||
const app = props.chat ? 'chat' : 'graph';
|
||||
const channelTitle =
|
||||
(channel && props.associations.graph?.[channel]?.metadata?.title) ||
|
||||
(channel && props.associations?.[app]?.[channel]?.metadata?.title) ||
|
||||
channel;
|
||||
|
||||
return (
|
||||
|
@ -12,7 +12,7 @@ import { Associations } from "~/types";
|
||||
|
||||
type DatedTimebox = [BigInteger, Timebox];
|
||||
|
||||
function filterNotification(groups: string[]) {
|
||||
function filterNotification(associations: Associations, groups: string[]) {
|
||||
if (groups.length === 0) {
|
||||
return () => true;
|
||||
}
|
||||
@ -23,6 +23,9 @@ function filterNotification(groups: string[]) {
|
||||
} else if ("group" in n.index) {
|
||||
const { group } = n.index.group;
|
||||
return groups.findIndex((g) => group === g) !== -1;
|
||||
} else if ("chat" in n.index) {
|
||||
const group = associations.chat[n.index.chat]?.["group-path"];
|
||||
return groups.findIndex((g) => group === g) !== -1;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
@ -56,7 +59,7 @@ export default function Inbox(props: {
|
||||
const notificationsByDay = f.flow(
|
||||
f.map<DatedTimebox>(([date, nots]) => [
|
||||
date,
|
||||
nots.filter(filterNotification(props.filter)),
|
||||
nots.filter(filterNotification(associations, props.filter)),
|
||||
]),
|
||||
f.groupBy<DatedTimebox>(([date]) =>
|
||||
moment(daToUnix(date)).format("DDMMYYYY")
|
||||
@ -75,6 +78,7 @@ export default function Inbox(props: {
|
||||
associations={props.associations}
|
||||
graphConfig={props.notificationsGraphConfig}
|
||||
groupConfig={props.notificationsGroupConfig}
|
||||
chatConfig={props.notificationsChatConfig}
|
||||
api={api}
|
||||
/>
|
||||
)}
|
||||
@ -92,6 +96,7 @@ export default function Inbox(props: {
|
||||
api={api}
|
||||
graphConfig={props.notificationsGraphConfig}
|
||||
groupConfig={props.notificationsGroupConfig}
|
||||
chatConfig={props.notificationsChatConfig}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
@ -119,6 +124,7 @@ function DaySection({
|
||||
api,
|
||||
groupConfig,
|
||||
graphConfig,
|
||||
chatConfig,
|
||||
}) {
|
||||
const calendar = latest
|
||||
? MOMENT_CALENDAR_DATE
|
||||
@ -143,6 +149,7 @@ function DaySection({
|
||||
<Notification
|
||||
graphConfig={graphConfig}
|
||||
groupConfig={groupConfig}
|
||||
chatConfig={chatConfig}
|
||||
api={api}
|
||||
associations={associations}
|
||||
notification={not}
|
||||
|
@ -8,12 +8,14 @@ import {
|
||||
NotificationGraphConfig,
|
||||
GroupNotificationsConfig,
|
||||
NotifIndex,
|
||||
Associations
|
||||
Associations,
|
||||
} from "~/types";
|
||||
import GlobalApi from "~/logic/api/global";
|
||||
import { StatelessAsyncAction } from "~/views/components/StatelessAsyncAction";
|
||||
import { GroupNotification } from "./group";
|
||||
import { GraphNotification } from "./graph";
|
||||
import { ChatNotification } from "./chat";
|
||||
import { BigInteger } from "big-integer";
|
||||
|
||||
interface NotificationProps {
|
||||
notification: IndexedNotification;
|
||||
@ -23,12 +25,14 @@ interface NotificationProps {
|
||||
archived: boolean;
|
||||
graphConfig: NotificationGraphConfig;
|
||||
groupConfig: GroupNotificationsConfig;
|
||||
chatConfig: string[];
|
||||
}
|
||||
|
||||
function getMuted(
|
||||
idx: NotifIndex,
|
||||
groups: GroupNotificationsConfig,
|
||||
graphs: NotificationGraphConfig
|
||||
graphs: NotificationGraphConfig,
|
||||
chat: string[]
|
||||
) {
|
||||
if ("graph" in idx) {
|
||||
const { graph } = idx.graph;
|
||||
@ -37,6 +41,9 @@ function getMuted(
|
||||
if ("group" in idx) {
|
||||
return _.findIndex(groups || [], (g) => g === idx.group.group) === -1;
|
||||
}
|
||||
if ("chat" in idx) {
|
||||
return _.findIndex(chat || [], (c) => c === idx.chat) === -1;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -48,6 +55,7 @@ function NotificationWrapper(props: {
|
||||
archived: boolean;
|
||||
graphConfig: NotificationGraphConfig;
|
||||
groupConfig: GroupNotificationsConfig;
|
||||
chatConfig: string[];
|
||||
}) {
|
||||
const { api, time, notif, children } = props;
|
||||
|
||||
@ -55,7 +63,12 @@ function NotificationWrapper(props: {
|
||||
return api.hark.archive(time, notif.index);
|
||||
}, [time, notif]);
|
||||
|
||||
const isMuted = getMuted(notif.index, props.groupConfig, props.graphConfig);
|
||||
const isMuted = getMuted(
|
||||
notif.index,
|
||||
props.groupConfig,
|
||||
props.graphConfig,
|
||||
props.chatConfig
|
||||
);
|
||||
|
||||
const onChangeMute = useCallback(async () => {
|
||||
const func = isMuted ? "unmute" : "mute";
|
||||
@ -84,19 +97,26 @@ export function Notification(props: NotificationProps) {
|
||||
const { notification, associations, archived } = props;
|
||||
const { read, contents, time } = notification.notification;
|
||||
|
||||
const Wrapper = ({ children }) => (
|
||||
<NotificationWrapper
|
||||
archived={archived}
|
||||
notif={notification}
|
||||
time={props.time}
|
||||
api={props.api}
|
||||
graphConfig={props.graphConfig}
|
||||
groupConfig={props.groupConfig}
|
||||
chatConfig={props.chatConfig}
|
||||
>
|
||||
{children}
|
||||
</NotificationWrapper>
|
||||
);
|
||||
|
||||
if ("graph" in notification.index) {
|
||||
const index = notification.index.graph;
|
||||
const c: GraphNotificationContents = (contents as any).graph;
|
||||
|
||||
return (
|
||||
<NotificationWrapper
|
||||
archived={archived}
|
||||
notif={notification}
|
||||
time={props.time}
|
||||
api={props.api}
|
||||
graphConfig={props.graphConfig}
|
||||
groupConfig={props.groupConfig}
|
||||
>
|
||||
<Wrapper>
|
||||
<GraphNotification
|
||||
api={props.api}
|
||||
index={index}
|
||||
@ -108,21 +128,14 @@ export function Notification(props: NotificationProps) {
|
||||
time={time}
|
||||
associations={associations}
|
||||
/>
|
||||
</NotificationWrapper>
|
||||
</Wrapper>
|
||||
);
|
||||
}
|
||||
if ("group" in notification.index) {
|
||||
const index = notification.index.group;
|
||||
const c: GroupNotificationContents = (contents as any).group;
|
||||
return (
|
||||
<NotificationWrapper
|
||||
archived={archived}
|
||||
notif={notification}
|
||||
time={props.time}
|
||||
api={props.api}
|
||||
graphConfig={props.graphConfig}
|
||||
groupConfig={props.groupConfig}
|
||||
>
|
||||
<Wrapper>
|
||||
<GroupNotification
|
||||
api={props.api}
|
||||
index={index}
|
||||
@ -134,7 +147,27 @@ export function Notification(props: NotificationProps) {
|
||||
time={time}
|
||||
associations={associations}
|
||||
/>
|
||||
</NotificationWrapper>
|
||||
</Wrapper>
|
||||
);
|
||||
}
|
||||
if ("chat" in notification.index) {
|
||||
const index = notification.index.chat;
|
||||
const c: ChatNotificationContents = (contents as any).chat;
|
||||
return (
|
||||
<Wrapper>
|
||||
<ChatNotification
|
||||
api={props.api}
|
||||
index={index}
|
||||
contents={c}
|
||||
contacts={props.contacts}
|
||||
read={read}
|
||||
archived={archived}
|
||||
groups={{}}
|
||||
timebox={props.time}
|
||||
time={time}
|
||||
associations={associations}
|
||||
/>
|
||||
</Wrapper>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
import React, { useCallback, useState } from "react";
|
||||
import _ from 'lodash';
|
||||
import { Box, Col, Text, Row } from "@tlon/indigo-react";
|
||||
import { Link, Switch, Route } from "react-router-dom";
|
||||
|
||||
|
@ -15,7 +15,7 @@ export function FormikOnBlur<
|
||||
) {
|
||||
const { values } = formikBag;
|
||||
formikBag.submitForm().then(() => {
|
||||
formikBag.resetForm({ values });
|
||||
formikBag.resetForm({ values, touched: {} });
|
||||
});
|
||||
}
|
||||
}, [
|
||||
|
@ -6,7 +6,7 @@ import { Dropdown } from "~/views/components/Dropdown";
|
||||
import { Association, NotificationGraphConfig } from "~/types";
|
||||
import GlobalApi from "~/logic/api/global";
|
||||
import { StatelessAsyncAction } from "~/views/components/StatelessAsyncAction";
|
||||
import {appIsGraph} from "~/logic/lib/util";
|
||||
import { appIsGraph } from "~/logic/lib/util";
|
||||
|
||||
const ChannelMenuItem = ({
|
||||
icon,
|
||||
@ -29,7 +29,8 @@ const ChannelMenuItem = ({
|
||||
interface ChannelMenuProps {
|
||||
association: Association;
|
||||
api: GlobalApi;
|
||||
notificationConfig: NotificationGraphConfig;
|
||||
graphNotificationConfig: NotificationGraphConfig;
|
||||
chatNotificationConfig: string[];
|
||||
}
|
||||
|
||||
export function ChannelMenu(props: ChannelMenuProps) {
|
||||
@ -49,10 +50,19 @@ export function ChannelMenu(props: ChannelMenuProps) {
|
||||
|
||||
const isOurs = ship.slice(1) === window.ship;
|
||||
|
||||
const isMuted =
|
||||
props.notificationConfig.watching.findIndex((a) => a === appPath) === -1;
|
||||
const isMuted = appIsGraph(app)
|
||||
? props.graphNotificationConfig.watching.findIndex((a) => a === appPath) ===
|
||||
-1
|
||||
: props.chatNotificationConfig.findIndex((a) => a === appPath) === -1;
|
||||
const onChangeMute = async () => {
|
||||
const func = isMuted ? "listenGraph" : "ignoreGraph";
|
||||
const func =
|
||||
association["app-name"] === "chat"
|
||||
? isMuted
|
||||
? "listenChat"
|
||||
: "ignoreChat"
|
||||
: isMuted
|
||||
? "listenGraph"
|
||||
: "ignoreGraph";
|
||||
await api.hark[func](appPath);
|
||||
};
|
||||
const onUnsubscribe = useCallback(async () => {
|
||||
@ -100,18 +110,16 @@ export function ChannelMenu(props: ChannelMenuProps) {
|
||||
borderRadius={1}
|
||||
borderColor="lightGray"
|
||||
>
|
||||
{appIsGraph(metadata.module) && (
|
||||
<ChannelMenuItem color="blue" icon="Inbox">
|
||||
<StatelessAsyncAction
|
||||
m="2"
|
||||
bg="white"
|
||||
name="notif"
|
||||
onClick={onChangeMute}
|
||||
>
|
||||
{isMuted ? "Unmute" : "Mute"} this channel
|
||||
</StatelessAsyncAction>
|
||||
</ChannelMenuItem>
|
||||
)}
|
||||
<ChannelMenuItem color="blue" icon="Inbox">
|
||||
<StatelessAsyncAction
|
||||
m="2"
|
||||
bg="white"
|
||||
name="notif"
|
||||
onClick={onChangeMute}
|
||||
>
|
||||
{isMuted ? "Unmute" : "Mute"} this channel
|
||||
</StatelessAsyncAction>
|
||||
</ChannelMenuItem>
|
||||
{isOurs ? (
|
||||
<>
|
||||
<ChannelMenuItem color="red" icon="TrashCan">
|
||||
|
@ -54,6 +54,7 @@ export function Resource(props: ResourceProps) {
|
||||
render={(routeProps) => (
|
||||
<ResourceSkeleton
|
||||
notificationsGraphConfig={props.notificationsGraphConfig}
|
||||
notificationsChatConfig={props.notificationsChatConfig}
|
||||
baseUrl={props.baseUrl}
|
||||
{...skelProps}
|
||||
atRoot
|
||||
|
@ -99,7 +99,8 @@ export function ResourceSkeleton(props: ResourceSkeletonProps) {
|
||||
</TruncatedBox>
|
||||
<Box flexGrow={1} />
|
||||
<ChannelMenu
|
||||
notificationConfig={props.notificationsGraphConfig}
|
||||
graphNotificationConfig={props.notificationsGraphConfig}
|
||||
chatNotificationConfig={props.notificationsChatConfig}
|
||||
association={association}
|
||||
api={api}
|
||||
/>
|
||||
|
Loading…
Reference in New Issue
Block a user