Merge pull request #3892 from urbit/lf/hark-chat-hook

hark-chat-hook: Notifications
This commit is contained in:
matildepark 2020-11-06 08:54:55 -05:00 committed by GitHub
commit f31dd0c047
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 608 additions and 69 deletions

View File

@ -5,7 +5,7 @@
/- glob /- glob
/+ default-agent, verb, dbug /+ 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))] +$ state-0 [%0 hash=@uv glob=(unit (each glob:glob tid=@ta))]
+$ all-states +$ all-states
$% state-0 $% state-0

View 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]
--

View File

@ -236,6 +236,10 @@
|= [existing=notification:store new=notification:store] |= [existing=notification:store new=notification:store]
^- notification:store ^- notification:store
?- -.contents.existing ?- -.contents.existing
::
%chat
?> ?=(%chat -.contents.new)
existing(list.contents (weld list.contents.existing list.contents.new))
:: ::
%graph %graph
?> ?=(%graph -.contents.new) ?> ?=(%graph -.contents.new)

View File

@ -24,6 +24,6 @@
<div id="portal-root"></div> <div id="portal-root"></div>
<script src="/~landscape/js/channel.js"></script> <script src="/~landscape/js/channel.js"></script>
<script src="/~landscape/js/session.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> </body>
</html> </html>

View 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)
==
--
--

View File

@ -1,5 +1,5 @@
/- sur=hark-store, post /- sur=hark-store, post
/+ resource, graph-store, group-store /+ resource, graph-store, group-store, chat-store
^? ^?
=< [. sur] =< [. sur]
=, sur =, sur
@ -11,6 +11,13 @@
%- of %- of
:~ graph+graph-index :~ graph+graph-index
group+group-index group+group-index
chat+chat-index
==
::
++ chat-index
%- ot
:~ chat+pa
mention+bo
== ==
:: ::
++ group-index ++ group-index
@ -106,6 +113,15 @@
?- -.index ?- -.index
%graph (graph-index +.index) %graph (graph-index +.index)
%group (group-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 ++ graph-index
@ -144,8 +160,15 @@
?- -.contents ?- -.contents
%graph (graph-contents +.contents) %graph (graph-contents +.contents)
%group (group-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 ++ graph-contents
|= =(list post:post) |= =(list post:post)
^- json ^- json

View File

@ -0,0 +1,13 @@
/+ *hark-chat-hook
|_ act=action
++ grad %noun
++ grow
|%
++ noun act
--
++ grab
|%
++ noun action
++ json action:dejs
--
--

View 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
--
--

View File

@ -0,0 +1,16 @@
^?
|%
::
+$ action
$% [?(%listen %ignore) chat=path]
[%set-mentions mentions=?]
==
::
+$ update
$%
action
$: %initial
watching=(set path)
==
==
--

View File

@ -1,10 +1,11 @@
/- *resource, graph-store, post, group-store, metadata-store /- *resource, graph-store, post, group-store, metadata-store, chat-store
^? ^?
|% |%
:: ::
+$ index +$ index
$% [%graph group=resource graph=resource module=@t description=@t] $% [%graph group=resource graph=resource module=@t description=@t]
[%group group=resource description=@t] [%group group=resource description=@t]
[%chat chat=path mention=?]
== ==
:: ::
+$ group-contents +$ group-contents
@ -19,6 +20,7 @@
+$ contents +$ contents
$% [%graph =(list post:post)] $% [%graph =(list post:post)]
[%group =(list group-contents)] [%group =(list group-contents)]
[%chat =(list envelope:chat-store)]
== ==
:: ::
+$ timebox +$ timebox

View File

@ -2,6 +2,7 @@ import BaseApi from "./base";
import { StoreState } from "../store/type"; import { StoreState } from "../store/type";
import { dateToDa, decToUd } from "../lib/util"; import { dateToDa, decToUd } from "../lib/util";
import {NotifIndex} from "~/types"; import {NotifIndex} from "~/types";
import { BigInteger } from 'big-integer';
export class HarkApi extends BaseApi<StoreState> { export class HarkApi extends BaseApi<StoreState> {
private harkAction(action: any): Promise<any> { private harkAction(action: any): Promise<any> {
@ -16,6 +17,10 @@ export class HarkApi extends BaseApi<StoreState> {
return this.action("hark-group-hook", "hark-group-hook-action", action); 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) { private actOnNotification(frond: string, intTime: BigInteger, index: NotifIndex) {
const time = decToUd(intTime.toString()); const time = decToUd(intTime.toString());
return this.harkAction({ return this.harkAction({
@ -26,12 +31,11 @@ export class HarkApi extends BaseApi<StoreState> {
}); });
} }
private graphHookAction(action: any) { async setMentions(mentions: boolean) {
return this.action("hark-graph-hook", "hark-graph-hook-action", action); await this.graphHookAction({
} 'set-mentions': mentions
});
setMentions(mentions: boolean) { return this.chatHookAction({
return this.graphHookAction({
'set-mentions': mentions 'set-mentions': mentions
}); });
} }
@ -73,6 +77,9 @@ export class HarkApi extends BaseApi<StoreState> {
const { group } = index.group; const { group } = index.group;
return this.ignoreGroup(group); return this.ignoreGroup(group);
} }
if('chat' in index) {
return this.ignoreChat(index.chat);
}
return Promise.resolve(); return Promise.resolve();
} }
@ -83,6 +90,9 @@ export class HarkApi extends BaseApi<StoreState> {
if('group' in index) { if('group' in index) {
return this.listenGroup(index.group.group); return this.listenGroup(index.group.group);
} }
if('chat' in index) {
return this.listenChat(index.chat);
}
return Promise.resolve(); return Promise.resolve();
} }
@ -98,6 +108,12 @@ export class HarkApi extends BaseApi<StoreState> {
}) })
} }
ignoreChat(chat: string) {
return this.chatHookAction({
ignore: chat
});
}
listenGroup(group: string) { listenGroup(group: string) {
return this.groupHookAction({ 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) { async getTimeSubset(start?: Date, end?: Date) {
const s = start ? dateToDa(start) : "-"; const s = start ? dateToDa(start) : "-";
const e = end ? dateToDa(end) : "-"; const e = end ? dateToDa(end) : "-";

View File

@ -1,6 +1,5 @@
import { import {
Notifications, Notifications,
Notification,
NotifIndex, NotifIndex,
NotificationGraphConfig, NotificationGraphConfig,
GroupNotificationsConfig, GroupNotificationsConfig,
@ -14,6 +13,7 @@ type HarkState = {
notificationsCount: number; notificationsCount: number;
notificationsGraphConfig: NotificationGraphConfig; notificationsGraphConfig: NotificationGraphConfig;
notificationsGroupConfig: GroupNotificationsConfig; notificationsGroupConfig: GroupNotificationsConfig;
notificationsChatConfig: string[];
}; };
export const HarkReducer = (json: any, state: HarkState) => { export const HarkReducer = (json: any, state: HarkState) => {
@ -36,8 +36,39 @@ export const HarkReducer = (json: any, state: HarkState) => {
groupListen(groupHookData, state); groupListen(groupHookData, state);
groupIgnore(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) { function groupInitial(json: any, state: HarkState) {
const data = _.get(json, "initial", false); const data = _.get(json, "initial", false);
if (data) { if (data) {
@ -177,6 +208,9 @@ function notifIdxEqual(a: NotifIndex, b: NotifIndex) {
a.group.group === b.group.group && a.group.group === b.group.group &&
a.group.description === b.group.description 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; return false;
} }

View File

@ -100,6 +100,7 @@ export default class GlobalStore extends BaseStore<StoreState> {
notifications: new BigIntOrderedMap<Timebox>(), notifications: new BigIntOrderedMap<Timebox>(),
archivedNotifications: new BigIntOrderedMap<Timebox>(), archivedNotifications: new BigIntOrderedMap<Timebox>(),
notificationsGroupConfig: [], notificationsGroupConfig: [],
notificationsChatConfig: [],
notificationsGraphConfig: { notificationsGraphConfig: {
watchOnSelf: false, watchOnSelf: false,
mentions: false, mentions: false,

View File

@ -62,6 +62,7 @@ export interface StoreState {
notifications: Notifications; notifications: Notifications;
notificationsGraphConfig: NotificationGraphConfig; notificationsGraphConfig: NotificationGraphConfig;
notificationsGroupConfig: GroupNotificationsConfig; notificationsGroupConfig: GroupNotificationsConfig;
notificationsChatConfig: string[];
notificationsCount: number, notificationsCount: number,
doNotDisturb: boolean; doNotDisturb: boolean;
} }

View File

@ -54,6 +54,7 @@ export default class GlobalSubscription extends BaseSubscription<StoreState> {
this.subscribe('/updates', 'hark-store'); this.subscribe('/updates', 'hark-store');
this.subscribe('/updates', 'hark-graph-hook'); this.subscribe('/updates', 'hark-graph-hook');
this.subscribe('/updates', 'hark-group-hook'); this.subscribe('/updates', 'hark-group-hook');
this.subscribe('/updates', 'hark-chat-hook');
} }
restart() { restart() {

View File

@ -2,6 +2,7 @@ import _ from "lodash";
import { Post } from "./graph-update"; import { Post } from "./graph-update";
import { GroupUpdate } from "./group-update"; import { GroupUpdate } from "./group-update";
import { BigIntOrderedMap } from "~/logic/lib/BigIntOrderedMap"; import { BigIntOrderedMap } from "~/logic/lib/BigIntOrderedMap";
import { Envelope } from './chat-update';
type GraphNotifDescription = "link" | "comment"; type GraphNotifDescription = "link" | "comment";
@ -17,17 +18,26 @@ export interface GroupNotifIndex {
description: string; description: string;
} }
export interface ChatNotifIndex {
chat: string;
mention: boolean;
}
export type NotifIndex = export type NotifIndex =
| { graph: GraphNotifIndex } | { graph: GraphNotifIndex }
| { group: GroupNotifIndex }; | { group: GroupNotifIndex }
| { chat: ChatNotifIndex };
export type GraphNotificationContents = Post[]; export type GraphNotificationContents = Post[];
export type GroupNotificationContents = GroupUpdate[]; export type GroupNotificationContents = GroupUpdate[];
export type ChatNotificationContents = Envelope[];
export type NotificationContents = export type NotificationContents =
| { graph: GraphNotificationContents } | { graph: GraphNotificationContents }
| { group: GroupNotificationContents }; | { group: GroupNotificationContents }
| { chat: ChatNotificationContents };
interface Notification { interface Notification {
read: boolean; read: boolean;

View File

@ -1,4 +1,4 @@
import React, { useRef, useCallback } from "react"; import React, { useRef, useCallback, useEffect } from "react";
import { RouteComponentProps } from "react-router-dom"; import { RouteComponentProps } from "react-router-dom";
import { Col } from "@tlon/indigo-react"; import { Col } from "@tlon/indigo-react";
import _ from 'lodash'; import _ from 'lodash';
@ -94,6 +94,15 @@ export function ChatResource(props: ChatResourceProps) {
station, station,
]); ]);
const scrollTo = new URLSearchParams(location.search).get('msg');
useEffect(() => {
const clear = () => {
props.history.replace(location.pathname);
}
setTimeout(clear, 10000);
return clear;
}, [station]);
return ( return (
<Col {...bind} height="100%" overflow="hidden" position="relative"> <Col {...bind} height="100%" overflow="hidden" position="relative">
{dragging && <SubmitDragger />} {dragging && <SubmitDragger />}
@ -118,6 +127,7 @@ export function ChatResource(props: ChatResourceProps) {
hideNicknames={props.hideNicknames} hideNicknames={props.hideNicknames}
hideAvatars={props.hideAvatars} hideAvatars={props.hideAvatars}
location={props.location} location={props.location}
scrollTo={scrollTo ? parseInt(scrollTo, 10) : undefined}
/> />
<ChatInput <ChatInput
ref={chatInput} ref={chatInput}

View File

@ -31,8 +31,8 @@ export const DayBreak = ({ when }) => (
interface ChatMessageProps { interface ChatMessageProps {
measure(element): void; measure(element): void;
msg: Envelope | IMessage; msg: Envelope | IMessage;
previousMsg: Envelope | IMessage | undefined; previousMsg?: Envelope | IMessage;
nextMsg: Envelope | IMessage | undefined; nextMsg?: Envelope | IMessage;
isLastRead: boolean; isLastRead: boolean;
group: Group; group: Group;
association: Association; association: Association;
@ -48,6 +48,7 @@ interface ChatMessageProps {
unreadMarkerRef: React.RefObject<HTMLDivElement>; unreadMarkerRef: React.RefObject<HTMLDivElement>;
history: any; history: any;
api: any; api: any;
highlighted?: boolean;
} }
export default class ChatMessage extends Component<ChatMessageProps> { export default class ChatMessage extends Component<ChatMessageProps> {
@ -84,7 +85,8 @@ export default class ChatMessage extends Component<ChatMessageProps> {
isLastMessage, isLastMessage,
unreadMarkerRef, unreadMarkerRef,
history, history,
api api,
highlighted
} = this.props; } = this.props;
const renderSigil = Boolean((nextMsg && msg.author !== nextMsg.author) || !nextMsg || msg.number === 1); const renderSigil = Boolean((nextMsg && msg.author !== nextMsg.author) || !nextMsg || msg.number === 1);
@ -115,7 +117,8 @@ export default class ChatMessage extends Component<ChatMessageProps> {
isPending, isPending,
history, history,
api, api,
scrollWindow scrollWindow,
highlighted
}; };
const unreadContainerStyle = { const unreadContainerStyle = {
@ -124,6 +127,7 @@ export default class ChatMessage extends Component<ChatMessageProps> {
return ( return (
<Box <Box
bg={highlighted ? 'washedBlue' : 'white'}
width='100%' width='100%'
display='flex' display='flex'
flexWrap='wrap' flexWrap='wrap'
@ -165,6 +169,8 @@ interface MessageProps {
}; };
export class MessageWithSigil extends PureComponent<MessageProps> { export class MessageWithSigil extends PureComponent<MessageProps> {
isDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
render() { render() {
const { const {
msg, msg,
@ -176,8 +182,8 @@ export class MessageWithSigil extends PureComponent<MessageProps> {
hideAvatars, hideAvatars,
remoteContentPolicy, remoteContentPolicy,
measure, measure,
history,
api, api,
history,
scrollWindow scrollWindow
} = this.props; } = this.props;
@ -185,8 +191,8 @@ export class MessageWithSigil extends PureComponent<MessageProps> {
const contact = msg.author in contacts ? contacts[msg.author] : false; const contact = msg.author in contacts ? contacts[msg.author] : false;
const showNickname = !hideNicknames && contact && contact.nickname; const showNickname = !hideNicknames && contact && contact.nickname;
const name = showNickname ? contact.nickname : cite(msg.author); const name = showNickname ? contact.nickname : cite(msg.author);
const color = contact ? `#${uxToHex(contact.color)}` : '#000000'; const color = contact ? `#${uxToHex(contact.color)}` : this.isDark ? '#000000' :'#FFFFFF'
const sigilClass = contact ? '' : 'mix-blend-diff'; const sigilClass = contact ? '' : this.isDark ? 'mix-blend-diff' : 'mix-blend-darken';
let nameSpan = null; let nameSpan = null;
@ -213,7 +219,7 @@ export class MessageWithSigil extends PureComponent<MessageProps> {
scrollWindow={scrollWindow} scrollWindow={scrollWindow}
history={history} history={history}
api={api} 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 flexGrow='1' display='block' className="clamp-message">
<Box <Box

View File

@ -43,6 +43,7 @@ type ChatWindowProps = RouteComponentProps<{
hideNicknames: boolean; hideNicknames: boolean;
hideAvatars: boolean; hideAvatars: boolean;
remoteContentPolicy: LocalUpdateRemoteContentPolicy; remoteContentPolicy: LocalUpdateRemoteContentPolicy;
scrollTo?: number;
} }
interface ChatWindowState { interface ChatWindowState {
@ -84,6 +85,10 @@ export default class ChatWindow extends Component<ChatWindowProps, ChatWindowSta
window.addEventListener('focus', this.handleWindowFocus); window.addEventListener('focus', this.handleWindowFocus);
this.initialFetch(); this.initialFetch();
setTimeout(() => { setTimeout(() => {
if(this.props.scrollTo) {
this.scrollToUnread();
}
this.setState({ initialized: true }); this.setState({ initialized: true });
}, this.INITIALIZATION_MAX_TIME); }, this.INITIALIZATION_MAX_TIME);
} }
@ -167,8 +172,9 @@ export default class ChatWindow extends Component<ChatWindowProps, ChatWindowSta
} }
scrollToUnread() { scrollToUnread() {
const { mailboxSize, unreadCount } = this.props; const { mailboxSize, unreadCount, scrollTo } = this.props;
this.virtualList?.scrollToData(mailboxSize - unreadCount); const target = scrollTo || (mailboxSize - unreadCount);
this.virtualList?.scrollToData(target);
} }
dismissUnread() { dismissUnread() {
@ -297,7 +303,8 @@ export default class ChatWindow extends Component<ChatWindowProps, ChatWindowSta
const isPending: boolean = 'pending' in msg && Boolean(msg.pending); const isPending: boolean = 'pending' in msg && Boolean(msg.pending);
const isLastMessage: boolean = Boolean(index === lastMessage) const isLastMessage: boolean = Boolean(index === lastMessage)
const isLastRead: boolean = Boolean(!isLastMessage && index === this.state.lastRead); 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 ( return (
<ChatMessage <ChatMessage
key={index} key={index}

View File

@ -87,6 +87,10 @@ h2 {
mix-blend-mode: difference; mix-blend-mode: difference;
} }
.mix-blend-darken {
mix-blend-mode: darken;
}
.placeholder-inter::placeholder { .placeholder-inter::placeholder {
font-family: "Inter", sans-serif; font-family: "Inter", sans-serif;
} }

View 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>
);
}

View File

@ -42,6 +42,7 @@ export function Header(props: {
time: number; time: number;
read: boolean; read: boolean;
associations: Associations; associations: Associations;
chat?: boolean;
}) { }) {
const { description, channel, group, moduleIcon, read } = props; const { description, channel, group, moduleIcon, read } = props;
const contacts = props.contacts[group] || {}; const contacts = props.contacts[group] || {};
@ -70,8 +71,9 @@ export function Header(props: {
const groupTitle = const groupTitle =
props.associations.contacts?.[props.group]?.metadata?.title || props.group; props.associations.contacts?.[props.group]?.metadata?.title || props.group;
const app = props.chat ? 'chat' : 'graph';
const channelTitle = const channelTitle =
(channel && props.associations.graph?.[channel]?.metadata?.title) || (channel && props.associations?.[app]?.[channel]?.metadata?.title) ||
channel; channel;
return ( return (

View File

@ -12,7 +12,7 @@ import { Associations } from "~/types";
type DatedTimebox = [BigInteger, Timebox]; type DatedTimebox = [BigInteger, Timebox];
function filterNotification(groups: string[]) { function filterNotification(associations: Associations, groups: string[]) {
if (groups.length === 0) { if (groups.length === 0) {
return () => true; return () => true;
} }
@ -23,6 +23,9 @@ function filterNotification(groups: string[]) {
} else if ("group" in n.index) { } else if ("group" in n.index) {
const { group } = n.index.group; const { group } = n.index.group;
return groups.findIndex((g) => group === g) !== -1; 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; return true;
}; };
@ -56,7 +59,7 @@ export default function Inbox(props: {
const notificationsByDay = f.flow( const notificationsByDay = f.flow(
f.map<DatedTimebox>(([date, nots]) => [ f.map<DatedTimebox>(([date, nots]) => [
date, date,
nots.filter(filterNotification(props.filter)), nots.filter(filterNotification(associations, props.filter)),
]), ]),
f.groupBy<DatedTimebox>(([date]) => f.groupBy<DatedTimebox>(([date]) =>
moment(daToUnix(date)).format("DDMMYYYY") moment(daToUnix(date)).format("DDMMYYYY")
@ -75,6 +78,7 @@ export default function Inbox(props: {
associations={props.associations} associations={props.associations}
graphConfig={props.notificationsGraphConfig} graphConfig={props.notificationsGraphConfig}
groupConfig={props.notificationsGroupConfig} groupConfig={props.notificationsGroupConfig}
chatConfig={props.notificationsChatConfig}
api={api} api={api}
/> />
)} )}
@ -92,6 +96,7 @@ export default function Inbox(props: {
api={api} api={api}
graphConfig={props.notificationsGraphConfig} graphConfig={props.notificationsGraphConfig}
groupConfig={props.notificationsGroupConfig} groupConfig={props.notificationsGroupConfig}
chatConfig={props.notificationsChatConfig}
/> />
) )
)} )}
@ -119,6 +124,7 @@ function DaySection({
api, api,
groupConfig, groupConfig,
graphConfig, graphConfig,
chatConfig,
}) { }) {
const calendar = latest const calendar = latest
? MOMENT_CALENDAR_DATE ? MOMENT_CALENDAR_DATE
@ -143,6 +149,7 @@ function DaySection({
<Notification <Notification
graphConfig={graphConfig} graphConfig={graphConfig}
groupConfig={groupConfig} groupConfig={groupConfig}
chatConfig={chatConfig}
api={api} api={api}
associations={associations} associations={associations}
notification={not} notification={not}

View File

@ -8,12 +8,14 @@ import {
NotificationGraphConfig, NotificationGraphConfig,
GroupNotificationsConfig, GroupNotificationsConfig,
NotifIndex, NotifIndex,
Associations Associations,
} from "~/types"; } from "~/types";
import GlobalApi from "~/logic/api/global"; import GlobalApi from "~/logic/api/global";
import { StatelessAsyncAction } from "~/views/components/StatelessAsyncAction"; import { StatelessAsyncAction } from "~/views/components/StatelessAsyncAction";
import { GroupNotification } from "./group"; import { GroupNotification } from "./group";
import { GraphNotification } from "./graph"; import { GraphNotification } from "./graph";
import { ChatNotification } from "./chat";
import { BigInteger } from "big-integer";
interface NotificationProps { interface NotificationProps {
notification: IndexedNotification; notification: IndexedNotification;
@ -23,12 +25,14 @@ interface NotificationProps {
archived: boolean; archived: boolean;
graphConfig: NotificationGraphConfig; graphConfig: NotificationGraphConfig;
groupConfig: GroupNotificationsConfig; groupConfig: GroupNotificationsConfig;
chatConfig: string[];
} }
function getMuted( function getMuted(
idx: NotifIndex, idx: NotifIndex,
groups: GroupNotificationsConfig, groups: GroupNotificationsConfig,
graphs: NotificationGraphConfig graphs: NotificationGraphConfig,
chat: string[]
) { ) {
if ("graph" in idx) { if ("graph" in idx) {
const { graph } = idx.graph; const { graph } = idx.graph;
@ -37,6 +41,9 @@ function getMuted(
if ("group" in idx) { if ("group" in idx) {
return _.findIndex(groups || [], (g) => g === idx.group.group) === -1; return _.findIndex(groups || [], (g) => g === idx.group.group) === -1;
} }
if ("chat" in idx) {
return _.findIndex(chat || [], (c) => c === idx.chat) === -1;
}
return false; return false;
} }
@ -48,6 +55,7 @@ function NotificationWrapper(props: {
archived: boolean; archived: boolean;
graphConfig: NotificationGraphConfig; graphConfig: NotificationGraphConfig;
groupConfig: GroupNotificationsConfig; groupConfig: GroupNotificationsConfig;
chatConfig: string[];
}) { }) {
const { api, time, notif, children } = props; const { api, time, notif, children } = props;
@ -55,7 +63,12 @@ function NotificationWrapper(props: {
return api.hark.archive(time, notif.index); return api.hark.archive(time, notif.index);
}, [time, notif]); }, [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 onChangeMute = useCallback(async () => {
const func = isMuted ? "unmute" : "mute"; const func = isMuted ? "unmute" : "mute";
@ -84,11 +97,7 @@ export function Notification(props: NotificationProps) {
const { notification, associations, archived } = props; const { notification, associations, archived } = props;
const { read, contents, time } = notification.notification; const { read, contents, time } = notification.notification;
if ("graph" in notification.index) { const Wrapper = ({ children }) => (
const index = notification.index.graph;
const c: GraphNotificationContents = (contents as any).graph;
return (
<NotificationWrapper <NotificationWrapper
archived={archived} archived={archived}
notif={notification} notif={notification}
@ -96,7 +105,18 @@ export function Notification(props: NotificationProps) {
api={props.api} api={props.api}
graphConfig={props.graphConfig} graphConfig={props.graphConfig}
groupConfig={props.groupConfig} 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 (
<Wrapper>
<GraphNotification <GraphNotification
api={props.api} api={props.api}
index={index} index={index}
@ -108,21 +128,14 @@ export function Notification(props: NotificationProps) {
time={time} time={time}
associations={associations} associations={associations}
/> />
</NotificationWrapper> </Wrapper>
); );
} }
if ("group" in notification.index) { if ("group" in notification.index) {
const index = notification.index.group; const index = notification.index.group;
const c: GroupNotificationContents = (contents as any).group; const c: GroupNotificationContents = (contents as any).group;
return ( return (
<NotificationWrapper <Wrapper>
archived={archived}
notif={notification}
time={props.time}
api={props.api}
graphConfig={props.graphConfig}
groupConfig={props.groupConfig}
>
<GroupNotification <GroupNotification
api={props.api} api={props.api}
index={index} index={index}
@ -134,7 +147,27 @@ export function Notification(props: NotificationProps) {
time={time} time={time}
associations={associations} 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>
); );
} }

View File

@ -1,4 +1,5 @@
import React, { useCallback, useState } from "react"; import React, { useCallback, useState } from "react";
import _ from 'lodash';
import { Box, Col, Text, Row } from "@tlon/indigo-react"; import { Box, Col, Text, Row } from "@tlon/indigo-react";
import { Link, Switch, Route } from "react-router-dom"; import { Link, Switch, Route } from "react-router-dom";

View File

@ -15,7 +15,7 @@ export function FormikOnBlur<
) { ) {
const { values } = formikBag; const { values } = formikBag;
formikBag.submitForm().then(() => { formikBag.submitForm().then(() => {
formikBag.resetForm({ values }); formikBag.resetForm({ values, touched: {} });
}); });
} }
}, [ }, [

View File

@ -6,7 +6,7 @@ import { Dropdown } from "~/views/components/Dropdown";
import { Association, NotificationGraphConfig } from "~/types"; import { Association, NotificationGraphConfig } from "~/types";
import GlobalApi from "~/logic/api/global"; import GlobalApi from "~/logic/api/global";
import { StatelessAsyncAction } from "~/views/components/StatelessAsyncAction"; import { StatelessAsyncAction } from "~/views/components/StatelessAsyncAction";
import {appIsGraph} from "~/logic/lib/util"; import { appIsGraph } from "~/logic/lib/util";
const ChannelMenuItem = ({ const ChannelMenuItem = ({
icon, icon,
@ -29,7 +29,8 @@ const ChannelMenuItem = ({
interface ChannelMenuProps { interface ChannelMenuProps {
association: Association; association: Association;
api: GlobalApi; api: GlobalApi;
notificationConfig: NotificationGraphConfig; graphNotificationConfig: NotificationGraphConfig;
chatNotificationConfig: string[];
} }
export function ChannelMenu(props: ChannelMenuProps) { export function ChannelMenu(props: ChannelMenuProps) {
@ -49,10 +50,19 @@ export function ChannelMenu(props: ChannelMenuProps) {
const isOurs = ship.slice(1) === window.ship; const isOurs = ship.slice(1) === window.ship;
const isMuted = const isMuted = appIsGraph(app)
props.notificationConfig.watching.findIndex((a) => a === appPath) === -1; ? props.graphNotificationConfig.watching.findIndex((a) => a === appPath) ===
-1
: props.chatNotificationConfig.findIndex((a) => a === appPath) === -1;
const onChangeMute = async () => { 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); await api.hark[func](appPath);
}; };
const onUnsubscribe = useCallback(async () => { const onUnsubscribe = useCallback(async () => {
@ -100,7 +110,6 @@ export function ChannelMenu(props: ChannelMenuProps) {
borderRadius={1} borderRadius={1}
borderColor="lightGray" borderColor="lightGray"
> >
{appIsGraph(metadata.module) && (
<ChannelMenuItem color="blue" icon="Inbox"> <ChannelMenuItem color="blue" icon="Inbox">
<StatelessAsyncAction <StatelessAsyncAction
m="2" m="2"
@ -111,7 +120,6 @@ export function ChannelMenu(props: ChannelMenuProps) {
{isMuted ? "Unmute" : "Mute"} this channel {isMuted ? "Unmute" : "Mute"} this channel
</StatelessAsyncAction> </StatelessAsyncAction>
</ChannelMenuItem> </ChannelMenuItem>
)}
{isOurs ? ( {isOurs ? (
<> <>
<ChannelMenuItem color="red" icon="TrashCan"> <ChannelMenuItem color="red" icon="TrashCan">

View File

@ -54,6 +54,7 @@ export function Resource(props: ResourceProps) {
render={(routeProps) => ( render={(routeProps) => (
<ResourceSkeleton <ResourceSkeleton
notificationsGraphConfig={props.notificationsGraphConfig} notificationsGraphConfig={props.notificationsGraphConfig}
notificationsChatConfig={props.notificationsChatConfig}
baseUrl={props.baseUrl} baseUrl={props.baseUrl}
{...skelProps} {...skelProps}
atRoot atRoot

View File

@ -99,7 +99,8 @@ export function ResourceSkeleton(props: ResourceSkeletonProps) {
</TruncatedBox> </TruncatedBox>
<Box flexGrow={1} /> <Box flexGrow={1} />
<ChannelMenu <ChannelMenu
notificationConfig={props.notificationsGraphConfig} graphNotificationConfig={props.notificationsGraphConfig}
chatNotificationConfig={props.notificationsChatConfig}
association={association} association={association}
api={api} api={api}
/> />