chat-js: update to global store

This commit is contained in:
Liam Fitzgerald 2020-06-22 16:05:43 +10:00
parent 85aa12a5a9
commit 535f415ebd
11 changed files with 386 additions and 388 deletions

View File

@ -85,8 +85,8 @@ export default class App extends React.Component {
render={ p => (
<LaunchApp
ship={this.ship}
channel={channel}
selectedGroups={selectedGroups}
api={this.api}
{...state}
{...p}
/>
)}
@ -94,8 +94,9 @@ export default class App extends React.Component {
<Route path="/~chat" render={ p => (
<ChatApp
ship={this.ship}
channel={channel}
selectedGroups={selectedGroups}
api={this.api}
subscription={this.subscription}
{...state}
{...p}
/>
)}
@ -105,6 +106,7 @@ export default class App extends React.Component {
ship={this.ship}
channel={channel}
selectedGroups={selectedGroups}
subscription={this.subscription}
{...p}
/>
)}
@ -112,8 +114,9 @@ export default class App extends React.Component {
<Route path="/~groups" render={ p => (
<GroupsApp
ship={this.ship}
channel={channel}
selectedGroups={selectedGroups}
api={this.api}
subscription={this.subscription}
{...state}
{...p}
/>
)}
@ -132,8 +135,9 @@ export default class App extends React.Component {
<Route path="/~publish" render={ p => (
<PublishApp
ship={this.ship}
channel={channel}
selectedGroups={selectedGroups}
api={this.api}
subscription={this.subscription}
{...state}
{...p}
/>
)}

View File

@ -1,68 +1,62 @@
import React from 'react';
import { Route, Switch } from 'react-router-dom';
import React from "react";
import { Route, Switch } from "react-router-dom";
import ChatApi from '../../api/chat';
import ChatStore from '../../store/chat';
import ChatSubscription from '../../subscription/chat';
import "./css/custom.css";
import { Skeleton } from "./components/skeleton";
import { Sidebar } from "./components/sidebar";
import { ChatScreen } from "./components/chat";
import { MemberScreen } from "./components/member";
import { SettingsScreen } from "./components/settings";
import { NewScreen } from "./components/new";
import { JoinScreen } from "./components/join";
import { NewDmScreen } from "./components/new-dm";
import { PatpNoSig } from "../../types/noun";
import GlobalApi from "../../api/global";
import { StoreState } from "../../store/type";
import GlobalSubscription from "../../subscription/global";
import './css/custom.css';
type ChatAppProps = StoreState & {
ship: PatpNoSig;
api: GlobalApi;
subscription: GlobalSubscription;
};
import { Skeleton } from './components/skeleton';
import { Sidebar } from './components/sidebar';
import { ChatScreen } from './components/chat';
import { MemberScreen } from './components/member';
import { SettingsScreen } from './components/settings';
import { NewScreen } from './components/new';
import { JoinScreen } from './components/join';
import { NewDmScreen } from './components/new-dm';
export default class ChatApp extends React.Component<ChatAppProps, {}> {
totalUnreads = 0;
export default class ChatApp extends React.Component {
constructor(props) {
super(props);
this.store = new ChatStore();
this.state = this.store.state;
this.totalUnreads = 0;
this.resetControllers();
}
resetControllers() {
this.api = null;
this.subscription = null;
}
componentDidMount() {
document.title = 'OS1 - Chat';
document.title = "OS1 - Chat";
// preload spinner asset
new Image().src = '/~landscape/img/Spinner.png';
new Image().src = "/~landscape/img/Spinner.png";
this.props.subscription.startApp('chat');
this.store.setStateHandler(this.setState.bind(this));
const channel = new this.props.channel();
this.api = new ChatApi(this.props.ship, channel, this.store);
this.subscription = new ChatSubscription(this.store, this.api, channel);
this.subscription.start();
}
componentWillUnmount() {
this.subscription.delete();
this.store.clear();
this.store.setStateHandler(() => {});
this.resetControllers();
this.props.subscription.stopApp('chat');
}
render() {
const { state, props } = this;
const { props } = this;
const messagePreviews = {};
const unreads = {};
let totalUnreads = 0;
const selectedGroups = props.selectedGroups ? props.selectedGroups : [];
const associations = state.associations ? state.associations : { chat: {}, contacts: {} };
const associations = props.associations
? props.associations
: { chat: {}, contacts: {} };
Object.keys(state.inbox).forEach((stat) => {
const envelopes = state.inbox[stat].envelopes;
Object.keys(props.inbox).forEach((stat) => {
const envelopes = props.inbox[stat].envelopes;
if (envelopes.length === 0) {
messagePreviews[stat] = false;
@ -70,42 +64,61 @@ export default class ChatApp extends React.Component {
messagePreviews[stat] = envelopes[0];
}
const unread = Math.max(state.inbox[stat].config.length - state.inbox[stat].config.read, 0);
const unread = Math.max(
props.inbox[stat].config.length - props.inbox[stat].config.read,
0
);
unreads[stat] = Boolean(unread);
if (unread &&
(selectedGroups.length === 0 || selectedGroups.map(((e) => {
return e[0];
})).includes(associations.chat?.[stat]?.['group-path']) ||
associations.chat?.[stat]?.['group-path'].startsWith('/~/'))) {
totalUnreads += unread;
if (
unread &&
(selectedGroups.length === 0 ||
selectedGroups
.map((e) => {
return e[0];
})
.includes(associations.chat?.[stat]?.["group-path"]) ||
associations.chat?.[stat]?.["group-path"].startsWith("/~/"))
) {
totalUnreads += unread;
}
});
if (totalUnreads !== this.totalUnreads) {
document.title = totalUnreads > 0 ? `OS1 - Chat (${totalUnreads})` : 'OS1 - Chat';
document.title =
totalUnreads > 0 ? `OS1 - Chat (${totalUnreads})` : "OS1 - Chat";
this.totalUnreads = totalUnreads;
}
const invites = state.invites ? state.invites : { '/chat': {}, '/contacts': {} };
const contacts = state.contacts ? state.contacts : {};
const s3 = state.s3 ? state.s3 : {};
const {
invites,
s3,
sidebarShown,
inbox,
contacts,
permissions,
chatSynced,
api,
chatInitialized,
pendingMessages
} = props;
const renderChannelSidebar = (props, station) => (
const renderChannelSidebar = (props, station?) => (
<Sidebar
inbox={state.inbox}
inbox={inbox}
messagePreviews={messagePreviews}
associations={associations}
selectedGroups={selectedGroups}
contacts={contacts}
invites={invites['/chat'] || {}}
invites={invites["/chat"] || {}}
unreads={unreads}
api={this.api}
api={api}
station={station}
{...props}
/>
);
return (
<Switch>
<Route
@ -117,14 +130,14 @@ export default class ChatApp extends React.Component {
associations={associations}
invites={invites}
chatHideonMobile={true}
sidebarShown={state.sidebarShown}
sidebarShown={sidebarShown}
sidebar={renderChannelSidebar(props)}
>
<div className="h-100 w-100 overflow-x-hidden flex flex-column bg-white bg-gray0-d">
<div className="pl3 pr3 pt2 dt pb3 w-100 h-100">
<p className="f8 pt3 gray2 w-100 h-100 dtc v-mid tc">
Select, create, or join a chat to begin.
</p>
</p>
</div>
</div>
</Skeleton>
@ -143,15 +156,15 @@ export default class ChatApp extends React.Component {
invites={invites}
sidebarHideOnMobile={true}
sidebar={renderChannelSidebar(props)}
sidebarShown={state.sidebarShown}
sidebarShown={sidebarShown}
>
<NewDmScreen
api={this.api}
inbox={state.inbox || {}}
permissions={state.permissions || {}}
contacts={state.contacts || {}}
api={api}
inbox={inbox}
permissions={permissions || {}}
contacts={contacts || {}}
associations={associations.contacts}
chatSynced={state.chatSynced || {}}
chatSynced={chatSynced || {}}
autoCreate={ship}
{...props}
/>
@ -169,15 +182,15 @@ export default class ChatApp extends React.Component {
invites={invites}
sidebarHideOnMobile={true}
sidebar={renderChannelSidebar(props)}
sidebarShown={state.sidebarShown}
sidebarShown={sidebarShown}
>
<NewScreen
api={this.api}
inbox={state.inbox || {}}
permissions={state.permissions || {}}
contacts={state.contacts || {}}
api={api}
inbox={inbox || {}}
permissions={permissions || {}}
contacts={contacts || {}}
associations={associations.contacts}
chatSynced={state.chatSynced || {}}
chatSynced={chatSynced || {}}
{...props}
/>
</Skeleton>
@ -188,11 +201,10 @@ export default class ChatApp extends React.Component {
exact
path="/~chat/join/(~)?/:ship?/:station?"
render={(props) => {
let station =
`/${props.match.params.ship}/${props.match.params.station}`;
const sig = props.match.url.includes('/~/');
let station = `/${props.match.params.ship}/${props.match.params.station}`;
const sig = props.match.url.includes("/~/");
if (sig) {
station = '/~' + station;
station = "/~" + station;
}
return (
@ -201,13 +213,13 @@ export default class ChatApp extends React.Component {
invites={invites}
sidebarHideOnMobile={true}
sidebar={renderChannelSidebar(props)}
sidebarShown={state.sidebarShown}
sidebarShown={sidebarShown}
>
<JoinScreen
api={this.api}
inbox={state.inbox}
api={api}
inbox={inbox}
autoJoin={station}
chatSynced={state.chatSynced || {}}
chatSynced={chatSynced || {}}
{...props}
/>
</Skeleton>
@ -218,40 +230,41 @@ export default class ChatApp extends React.Component {
exact
path="/~chat/(popout)?/room/(~)?/:ship/:station+"
render={(props) => {
let station =
`/${props.match.params.ship}/${props.match.params.station}`;
const sig = props.match.url.includes('/~/');
let station = `/${props.match.params.ship}/${props.match.params.station}`;
const sig = props.match.url.includes("/~/");
if (sig) {
station = '/~' + station;
station = "/~" + station;
}
const mailbox = state.inbox[station] || {
const mailbox = inbox[station] || {
config: {
read: 0,
length: 0
length: 0,
},
envelopes: []
envelopes: [],
};
let roomContacts = {};
const associatedGroup =
station in associations['chat'] &&
'group-path' in associations.chat[station]
? associations.chat[station]['group-path']
: '';
station in associations["chat"] &&
"group-path" in associations.chat[station]
? associations.chat[station]["group-path"]
: "";
if ((associations.chat[station]) && (associatedGroup in contacts)) {
if (associations.chat[station] && associatedGroup in contacts) {
roomContacts = contacts[associatedGroup];
}
const association =
station in associations['chat'] ? associations.chat[station] : {};
station in associations["chat"] ? associations.chat[station] : {};
const permission =
station in state.permissions ? state.permissions[station] : {
who: new Set([]),
kind: 'white'
};
const popout = props.match.url.includes('/popout/');
station in permissions
? permissions[station]
: {
who: new Set([]),
kind: "white",
};
const popout = props.match.url.includes("/popout/");
return (
<Skeleton
@ -259,26 +272,25 @@ export default class ChatApp extends React.Component {
invites={invites}
sidebarHideOnMobile={true}
popout={popout}
sidebarShown={state.sidebarShown}
sidebarShown={sidebarShown}
sidebar={renderChannelSidebar(props, station)}
>
<ChatScreen
chatSynced={state.chatSynced}
chatSynced={chatSynced || {}}
station={station}
association={association}
api={this.api}
subscription={this.subscription}
api={api}
read={mailbox.config.read}
length={mailbox.config.length}
envelopes={mailbox.envelopes}
inbox={state.inbox}
inbox={inbox}
contacts={roomContacts}
permission={permission}
pendingMessages={state.pendingMessages}
pendingMessages={pendingMessages}
s3={s3}
popout={popout}
sidebarShown={state.sidebarShown}
chatInitialized={state.chatInitialized}
sidebarShown={sidebarShown}
chatInitialized={chatInitialized}
{...props}
/>
</Skeleton>
@ -290,39 +302,39 @@ export default class ChatApp extends React.Component {
path="/~chat/(popout)?/members/(~)?/:ship/:station+"
render={(props) => {
let station = `/${props.match.params.ship}/${props.match.params.station}`;
const sig = props.match.url.includes('/~/');
const sig = props.match.url.includes("/~/");
if (sig) {
station = '/~' + station;
station = "/~" + station;
}
const permission = state.permissions[station] || {
kind: '',
who: new Set([])
const permission = permissions[station] || {
kind: "",
who: new Set([]),
};
const popout = props.match.url.includes('/popout/');
const popout = props.match.url.includes("/popout/");
const association =
station in associations['chat'] ? associations.chat[station] : {};
station in associations["chat"] ? associations.chat[station] : {};
return (
<Skeleton
associations={associations}
invites={invites}
sidebarHideOnMobile={true}
sidebarShown={state.sidebarShown}
sidebarShown={sidebarShown}
popout={popout}
sidebar={renderChannelSidebar(props, station)}
>
<MemberScreen
{...props}
api={this.api}
api={api}
station={station}
association={association}
permission={permission}
contacts={contacts}
permissions={state.permissions}
permissions={permissions}
popout={popout}
sidebarShown={state.sidebarShown}
sidebarShown={sidebarShown}
/>
</Skeleton>
);
@ -332,22 +344,21 @@ export default class ChatApp extends React.Component {
exact
path="/~chat/(popout)?/settings/(~)?/:ship/:station+"
render={(props) => {
let station =
`/${props.match.params.ship}/${props.match.params.station}`;
const sig = props.match.url.includes('/~/');
let station = `/${props.match.params.ship}/${props.match.params.station}`;
const sig = props.match.url.includes("/~/");
if (sig) {
station = '/~' + station;
station = "/~" + station;
}
const popout = props.match.url.includes('/popout/');
const popout = props.match.url.includes("/popout/");
const permission = state.permissions[station] || {
kind: '',
who: new Set([])
const permission = permissions[station] || {
kind: "",
who: new Set([]),
};
const association =
station in associations['chat'] ? associations.chat[station] : {};
station in associations["chat"] ? associations.chat[station] : {};
return (
<Skeleton
@ -355,7 +366,7 @@ export default class ChatApp extends React.Component {
invites={invites}
sidebarHideOnMobile={true}
popout={popout}
sidebarShown={state.sidebarShown}
sidebarShown={sidebarShown}
sidebar={renderChannelSidebar(props, station)}
>
<SettingsScreen
@ -363,19 +374,19 @@ export default class ChatApp extends React.Component {
station={station}
association={association}
permission={permission}
permissions={state.permissions || {}}
contacts={state.contacts || {}}
permissions={permissions || {}}
contacts={contacts || {}}
associations={associations.contacts}
api={this.api}
inbox={state.inbox}
api={api}
inbox={inbox}
popout={popout}
sidebarShown={state.sidebarShown}
sidebarShown={sidebarShown}
/>
</Skeleton>
);
}}
/>
</Switch>
);
);
}
}

View File

@ -1,19 +1,26 @@
import React, { Component } from 'react';
import _ from 'lodash';
import moment from 'moment';
import React, { Component } from "react";
import _ from "lodash";
import moment from "moment";
import { Link } from 'react-router-dom';
import { Link, RouteComponentProps } from "react-router-dom";
import { ResubscribeElement } from './lib/resubscribe-element';
import { BacklogElement } from './lib/backlog-element';
import { Message } from './lib/message';
import { SidebarSwitcher } from '../../../components/SidebarSwitch';
import { ChatTabBar } from './lib/chat-tabbar';
import { ChatInput } from './lib/chat-input';
import { UnreadNotice } from './lib/unread-notice';
import { deSig } from '../../../lib/util';
import { ResubscribeElement } from "./lib/resubscribe-element";
import { BacklogElement } from "./lib/backlog-element";
import { Message } from "./lib/message";
import { SidebarSwitcher } from "../../../components/SidebarSwitch";
import { ChatTabBar } from "./lib/chat-tabbar";
import { ChatInput } from "./lib/chat-input";
import { UnreadNotice } from "./lib/unread-notice";
import { deSig } from "../../../lib/util";
import { ChatHookUpdate } from "../../../types/chat-hook-update";
import ChatApi from "../../../api/chat";
import { Inbox, Envelope } from "../../../types/chat-update";
import { Contacts } from "../../../types/contact-update";
import { Path, Patp } from "../../../types/noun";
import GlobalApi from "../../../api/global";
import { Association } from "../../../types/metadata-update";
function getNumPending(props) {
function getNumPending(props: any) {
const result = props.pendingMessages.has(props.station)
? props.pendingMessages.get(props.station).length
: 0;
@ -25,26 +32,32 @@ const DEFAULT_BACKLOG_SIZE = 300;
const MAX_BACKLOG_SIZE = 1000;
function scrollIsAtTop(container) {
if ((navigator.userAgent.includes("Safari") &&
if (
(navigator.userAgent.includes("Safari") &&
navigator.userAgent.includes("Chrome")) ||
navigator.userAgent.includes("Firefox")
navigator.userAgent.includes("Firefox")
) {
return container.scrollTop === 0;
} else if (navigator.userAgent.includes("Safari")) {
return container.scrollHeight + Math.round(container.scrollTop) <=
container.clientHeight + 10;
return (
container.scrollHeight + Math.round(container.scrollTop) <=
container.clientHeight + 10
);
} else {
return false;
}
}
function scrollIsAtBottom(container) {
if ((navigator.userAgent.includes("Safari") &&
if (
(navigator.userAgent.includes("Safari") &&
navigator.userAgent.includes("Chrome")) ||
navigator.userAgent.includes("Firefox")
navigator.userAgent.includes("Firefox")
) {
return container.scrollHeight - Math.round(container.scrollTop) <=
container.clientHeight + 10;
return (
container.scrollHeight - Math.round(container.scrollTop) <=
container.clientHeight + 10
);
} else if (navigator.userAgent.includes("Safari")) {
return container.scrollTop === 0;
} else {
@ -52,7 +65,50 @@ function scrollIsAtBottom(container) {
}
}
export class ChatScreen extends Component {
type IMessage = Envelope & { pending?: boolean };
type ChatScreenProps = RouteComponentProps<{
ship: Patp;
station: string;
}> & {
chatSynced: ChatHookUpdate;
station: any;
association: Association;
api: GlobalApi;
read: number;
length: number;
inbox: Inbox;
contacts: Contacts;
permission: any;
pendingMessages: Map<Path, Envelope[]>;
s3: any;
popout: boolean;
sidebarShown: boolean;
chatInitialized: boolean;
envelopes: Envelope[];
};
interface ChatScreenState {
numPages: number;
scrollLocked: boolean;
read: number;
active: boolean;
lastScrollHeight: number | null;
}
export class ChatScreen extends Component<ChatScreenProps, ChatScreenState> {
hasAskedForMessages = false;
lastNumPending = 0;
scrollContainer: HTMLElement | null = null;
unreadMarker = null;
scrolledToMarker = false;
activityTimeout: NodeJS.Timeout | null = null;
scrollElement: HTMLElement | null = null;
constructor(props) {
super(props);
@ -65,29 +121,22 @@ export class ChatScreen extends Component {
lastScrollHeight: null,
};
this.hasAskedForMessages = false;
this.lastNumPending = 0;
this.scrollContainer = null;
this.onScroll = this.onScroll.bind(this);
this.unreadMarker = null;
this.scrolledToMarker = false;
this.setUnreadMarker = this.setUnreadMarker.bind(this);
this.activityTimeout = true;
this.handleActivity = this.handleActivity.bind(this);
this.setInactive = this.setInactive.bind(this);
moment.updateLocale('en', {
moment.updateLocale("en", {
calendar: {
sameDay: '[Today]',
nextDay: '[Tomorrow]',
nextWeek: 'dddd',
lastDay: '[Yesterday]',
lastWeek: '[Last] dddd',
sameElse: 'DD/MM/YYYY'
}
sameDay: "[Today]",
nextDay: "[Tomorrow]",
nextWeek: "dddd",
lastDay: "[Yesterday]",
lastWeek: "[Last] dddd",
sameElse: "DD/MM/YYYY",
},
});
}
@ -104,17 +153,17 @@ export class ChatScreen extends Component {
document.removeEventListener("mousedown", this.handleActivity, false);
document.removeEventListener("keypress", this.handleActivity, false);
document.removeEventListener("touchmove", this.handleActivity, false);
if(this.activityTimeout) {
if (this.activityTimeout) {
clearTimeout(this.activityTimeout);
}
}
handleActivity() {
if(!this.state.active) {
if (!this.state.active) {
this.setState({ active: true });
}
if(this.activityTimeout) {
if (this.activityTimeout) {
clearTimeout(this.activityTimeout);
}
@ -139,13 +188,13 @@ export class ChatScreen extends Component {
const unreadUnloaded = unread - props.envelopes.length;
const excessUnread = unreadUnloaded > MAX_BACKLOG_SIZE;
if(!excessUnread && unreadUnloaded + 20 > DEFAULT_BACKLOG_SIZE) {
if (!excessUnread && unreadUnloaded + 20 > DEFAULT_BACKLOG_SIZE) {
this.askForMessages(unreadUnloaded + 20);
} else {
this.askForMessages(DEFAULT_BACKLOG_SIZE);
}
if(excessUnread || props.read === props.length){
if (excessUnread || props.read === props.length) {
this.scrolledToMarker = true;
this.setState(
{
@ -156,7 +205,7 @@ export class ChatScreen extends Component {
}
);
} else {
this.setState({ scrollLocked: true, numPages: Math.ceil(unread/100) });
this.setState({ scrollLocked: true, numPages: Math.ceil(unread / 100) });
}
}
@ -168,34 +217,36 @@ export class ChatScreen extends Component {
prevProps.match.params.ship !== props.match.params.ship
) {
this.receivedNewChat();
} else if (props.chatInitialized &&
!(props.station in props.inbox) &&
(Boolean(props.chatSynced) && !(props.station in props.chatSynced))) {
props.history.push('/~chat');
} else if (
props.envelopes.length >= prevProps.envelopes.length + 10
props.chatInitialized &&
!(props.station in props.inbox) &&
Boolean(props.chatSynced) &&
!(props.station in props.chatSynced)
) {
props.history.push("/~chat");
} else if (props.envelopes.length >= prevProps.envelopes.length + 10) {
this.hasAskedForMessages = false;
} else if(props.length !== prevProps.length &&
prevProps.length === prevState.read &&
state.active
} else if (
props.length !== prevProps.length &&
prevProps.length === prevState.read &&
state.active
) {
this.setState({ read: props.length });
this.props.api.chat.read(this.props.station);
}
if(!prevProps.chatInitialized && props.chatInitialized) {
if (!prevProps.chatInitialized && props.chatInitialized) {
this.receivedNewChat();
}
if (
(props.length !== prevProps.length ||
props.envelopes.length !== prevProps.envelopes.length ||
getNumPending(props) !== this.lastNumPending ||
state.numPages !== prevState.numPages)
props.length !== prevProps.length ||
props.envelopes.length !== prevProps.envelopes.length ||
getNumPending(props) !== this.lastNumPending ||
state.numPages !== prevState.numPages
) {
this.scrollToBottom();
if(navigator.userAgent.includes("Firefox")) {
if (navigator.userAgent.includes("Firefox")) {
this.recalculateScrollTop();
}
@ -219,7 +270,7 @@ export class ChatScreen extends Component {
if (start > 0) {
const end = start + size < props.length ? start + size : props.length;
this.hasAskedForMessages = true;
props.subscription.fetchMessages(start + 1, end, props.station);
props.api.chat.fetchMessages(start + 1, end, props.station);
}
}
@ -231,31 +282,31 @@ export class ChatScreen extends Component {
// Restore chat position on FF when new messages come in
recalculateScrollTop() {
if(!this.scrollContainer) {
const { lastScrollHeight } = this.state;
if (!this.scrollContainer || !lastScrollHeight) {
return;
}
const { lastScrollHeight } = this.state;
const target = this.scrollContainer;
const newScrollTop = this.scrollContainer.scrollHeight - lastScrollHeight;
if(target.scrollTop !== 0 || newScrollTop === target.scrollTop) {
if (target.scrollTop !== 0 || newScrollTop === target.scrollTop) {
return;
}
target.scrollTop = target.scrollHeight - lastScrollHeight;
}
onScroll(e) {
if(scrollIsAtTop(e.target)) {
if (scrollIsAtTop(e.target)) {
// Save scroll position for FF
if (navigator.userAgent.includes('Firefox')) {
if (navigator.userAgent.includes("Firefox")) {
this.setState({
lastScrollHeight: e.target.scrollHeight
lastScrollHeight: e.target.scrollHeight,
});
}
this.setState(
{
numPages: this.state.numPages + 1,
scrollLocked: true
scrollLocked: true,
},
() => {
this.askForMessages(DEFAULT_BACKLOG_SIZE);
@ -265,21 +316,20 @@ export class ChatScreen extends Component {
this.dismissUnread();
this.setState({
numPages: 1,
scrollLocked: false
scrollLocked: false,
});
}
}
setUnreadMarker(ref) {
if(ref && !this.scrolledToMarker) {
if (ref && !this.scrolledToMarker) {
this.setState({ scrollLocked: true }, () => {
ref.scrollIntoView({ block: 'center' });
if(ref.offsetParent &&
scrollIsAtBottom(ref.offsetParent)) {
ref.scrollIntoView({ block: "center" });
if (ref.offsetParent && scrollIsAtBottom(ref.offsetParent)) {
this.dismissUnread();
this.setState({
numPages: 1,
scrollLocked: false
scrollLocked: false,
});
}
});
@ -298,38 +348,32 @@ export class ChatScreen extends Component {
const { props, state } = this;
let messages = props.envelopes.slice(0);
let messages: IMessage[] = props.envelopes.slice(0);
const lastMsgNum = messages.length > 0 ? messages.length : 0;
if (messages.length > 100 * state.numPages) {
messages = messages.slice(0, 100 * state.numPages);
}
const pendingMessages = props.pendingMessages.has(props.station)
? props.pendingMessages.get(props.station)
: [];
pendingMessages.map((value) => {
return (value.pending = true);
});
const pendingMessages: IMessage[] = (
props.pendingMessages.get(props.station) || []
).map((value) => ({ ...value, pending: true }));
messages = pendingMessages.concat(messages);
const messageElements = messages.map((msg, i) => {
// Render sigil if previous message is not by the same sender
const aut = ['author'];
const aut = ["author"];
const renderSigil =
_.get(messages[i + 1], aut) !==
_.get(msg, aut, msg.author);
_.get(messages[i + 1], aut) !== _.get(msg, aut, msg.author);
const paddingTop = renderSigil;
const paddingBot =
_.get(messages[i - 1], aut) !==
_.get(msg, aut, msg.author);
_.get(messages[i - 1], aut) !== _.get(msg, aut, msg.author);
const when = ['when'];
const when = ["when"];
const dayBreak =
moment(_.get(messages[i+1], when)).format('YYYY.MM.DD') !==
moment(_.get(messages[i], when)).format('YYYY.MM.DD');
moment(_.get(messages[i + 1], when)).format("YYYY.MM.DD") !==
moment(_.get(messages[i], when)).format("YYYY.MM.DD");
const messageElem = (
<Message
@ -343,33 +387,39 @@ export class ChatScreen extends Component {
group={props.association}
/>
);
if(unread > 0 && i === unread - 1) {
if (unread > 0 && i === unread - 1) {
return (
<>
{messageElem}
<div key={'unreads'+ msg.uid} ref={this.setUnreadMarker} className="mv2 green2 flex items-center f9">
<div
key={"unreads" + msg.uid}
ref={this.setUnreadMarker}
className="mv2 green2 flex items-center f9"
>
<hr className="dn-s ma0 w2 b--green2 bt-0" />
<p className="mh4">
New messages below
</p>
<p className="mh4">New messages below</p>
<hr className="ma0 flex-grow-1 b--green2 bt-0" />
{ dayBreak && (
<p className="gray2 mh4">
{moment(_.get(messages[i], when)).calendar()}
</p>
{dayBreak && (
<p className="gray2 mh4">
{moment(_.get(messages[i], when)).calendar()}
</p>
)}
<hr style={{ width: 'calc(50% - 48px)' }} className="b--green2 ma0 bt-0" />
<hr
style={{ width: "calc(50% - 48px)" }}
className="b--green2 ma0 bt-0"
/>
</div>
</>
);
} else if(dayBreak) {
} else if (dayBreak) {
return (
<>
{messageElem}
<div key={'daybreak' + msg.uid} className="pv3 gray2 b--gray2 flex items-center justify-center f9 ">
<p>
{moment(_.get(messages[i], when)).calendar()}
</p>
<div
key={"daybreak" + msg.uid}
className="pv3 gray2 b--gray2 flex items-center justify-center f9 "
>
<p>{moment(_.get(messages[i], when)).calendar()}</p>
</div>
</>
);
@ -378,47 +428,47 @@ export class ChatScreen extends Component {
}
});
if (navigator.userAgent.includes('Firefox')) {
if (navigator.userAgent.includes("Firefox")) {
return (
<div className="relative overflow-y-scroll h-100" onScroll={this.onScroll}
ref={(e) => {
this.scrollContainer = e;
}}
<div
className="relative overflow-y-scroll h-100"
onScroll={this.onScroll}
ref={(e) => {
this.scrollContainer = e;
}}
>
<div
className="bg-white bg-gray0-d pt3 pb2 flex flex-column-reverse"
style={{ resize: 'vertical' }}
style={{ resize: "vertical" }}
>
<div
ref={(el) => {
this.scrollElement = el;
}}
></div>
{(props.chatInitialized &&
!(props.station in props.inbox)) && (
<BacklogElement />
{props.chatInitialized && !(props.station in props.inbox) && (
<BacklogElement />
)}
{props.chatSynced &&
!(props.station in props.chatSynced) &&
messages.length > 0 ? (
<ResubscribeElement
api={props.api}
host={props.match.params.ship}
station={props.station}
/>
) : (
<div />
)}
{(
props.chatSynced &&
!(props.station in props.chatSynced) &&
(messages.length > 0)
) ? (
<ResubscribeElement
api={props.api}
host={props.match.params.ship}
station={props.station}
/>
) : (<div />)
}
{messageElements}
</div>
</div>
);
} else {
} else {
return (
<div
className="overflow-y-scroll bg-white bg-gray0-d pt3 pb2 flex flex-column-reverse relative"
style={{ height: '100%', resize: 'vertical' }}
style={{ height: "100%", resize: "vertical" }}
onScroll={this.onScroll}
>
<div
@ -426,26 +476,24 @@ ref={(e) => {
this.scrollElement = el;
}}
></div>
{(props.chatInitialized &&
!(props.station in props.inbox)) && (
<BacklogElement />
{props.chatInitialized && !(props.station in props.inbox) && (
<BacklogElement />
)}
{props.chatSynced &&
!(props.station in props.chatSynced) &&
messages.length > 0 ? (
<ResubscribeElement
api={props.api}
host={props.match.params.ship}
station={props.station}
/>
) : (
<div />
)}
{(
props.chatSynced &&
!(props.station in props.chatSynced) &&
(messages.length > 0)
) ? (
<ResubscribeElement
api={props.api}
host={props.match.params.ship}
station={props.station}
/>
) : (<div />)
}
{messageElements}
</div>
);
}
}
}
render() {
@ -457,16 +505,16 @@ ref={(e) => {
const group = Array.from(props.permission.who.values());
const isinPopout = props.popout ? 'popout/' : '';
const isinPopout = props.popout ? "popout/" : "";
const ownerContact = (window.ship in props.contacts)
? props.contacts[window.ship] : false;
const ownerContact =
window.ship in props.contacts ? props.contacts[window.ship] : false;
let title = props.station.substr(1);
if (props.association && 'metadata' in props.association) {
if (props.association && "metadata" in props.association) {
title =
props.association.metadata.title !== ''
props.association.metadata.title !== ""
? props.association.metadata.title
: props.station.substr(1);
}
@ -475,8 +523,8 @@ ref={(e) => {
const unreadMsg = unread > 0 && messages[unread - 1];
const showUnreadNotice = props.length !== props.read && props.read === state.read;
const showUnreadNotice =
props.length !== props.read && props.read === state.read;
return (
<div
@ -485,14 +533,16 @@ ref={(e) => {
>
<div
className="w-100 dn-m dn-l dn-xl inter pt4 pb6 pl3 f8"
style={{ height: '1rem' }}
style={{ height: "1rem" }}
>
<Link to="/~chat/">{'⟵ All Chats'}</Link>
<Link to="/~chat/">{"⟵ All Chats"}</Link>
</div>
<div
className={'pl4 pt2 bb b--gray4 b--gray1-d bg-gray0-d flex relative ' +
'overflow-x-auto overflow-y-hidden flex-shrink-0 '}
className={
"pl4 pt2 bb b--gray4 b--gray1-d bg-gray0-d flex relative " +
"overflow-x-auto overflow-y-hidden flex-shrink-0 "
}
style={{ height: 48 }}
>
<SidebarSwitcher
@ -500,13 +550,16 @@ ref={(e) => {
popout={props.popout}
api={props.api}
/>
<Link to={'/~chat/' + isinPopout + 'room' + props.station}
className="pt2 white-d"
<Link
to={"/~chat/" + isinPopout + "room" + props.station}
className="pt2 white-d"
>
<h2
className={'dib f9 fw4 lh-solid v-top ' +
((title === props.station.substr(1)) ? 'mono' : '')}
style={{ width: 'max-content' }}
className={
"dib f9 fw4 lh-solid v-top " +
(title === props.station.substr(1) ? "mono" : "")
}
style={{ width: "max-content" }}
>
{title}
</h2>
@ -520,13 +573,13 @@ ref={(e) => {
api={props.api}
/>
</div>
{ !!unreadMsg && showUnreadNotice && (
{!!unreadMsg && showUnreadNotice && (
<UnreadNotice
unread={unread}
unreadMsg={unreadMsg}
onRead={() => this.dismissUnread()}
/>
) }
)}
{this.chatWindow(unread)}
<ChatInput
api={props.api}

View File

@ -45,7 +45,7 @@ export class JoinScreen extends Component {
this.setState({
station,
awaiting: true
}, () => props.api.chatView.join(ship, station, true));
}, () => props.api.chat.join(ship, station, true));
}
if (state.station in props.inbox ||
@ -78,7 +78,7 @@ export class JoinScreen extends Component {
station,
awaiting: true
}, () => {
props.api.chatView.join(ship, station, true);
props.api.chat.join(ship, station, true);
});
}

View File

@ -2,7 +2,7 @@ import React, { Component } from 'react';
export class ResubscribeElement extends Component {
onClickResubscribe() {
this.props.api.chatHook.addSynced(
this.props.api.chat.addSynced(
this.props.host,
this.props.station,
true);

View File

@ -2,11 +2,11 @@ import React, { Component } from 'react';
export class SidebarInvite extends Component {
onAccept() {
this.props.api.invite.accept(this.props.uid);
this.props.api.invite.accept('/chat', this.props.uid);
}
onDecline() {
this.props.api.invite.decline(this.props.uid);
this.props.api.invite.decline('/chat', this.props.uid);
}
render() {

View File

@ -63,7 +63,7 @@ export class NewDmScreen extends Component {
},
() => {
const groupPath = station;
props.api.chatView.create(
props.api.chat.create(
`~${window.ship} <-> ~${state.ship}`,
'',
station,

View File

@ -146,7 +146,7 @@ export class NewScreen extends Component {
if (state.groups.length > 0) {
groupPath = state.groups[0];
}
const submit = props.api.chatView.create(
const submit = props.api.chat.create(
state.title,
state.description,
appPath,

View File

@ -108,7 +108,8 @@ export class SettingsScreen extends Component {
if (chatOwner) {
this.setState({ awaiting: true, type: 'Editing chat...' }, (() => {
props.api.metadata.add(
props.api.metadata.metadataAdd(
'chat',
association['app-path'],
association['group-path'],
association.metadata.title,
@ -133,7 +134,7 @@ export class SettingsScreen extends Component {
? 'Deleting chat...'
: 'Leaving chat...'
}, (() => {
props.api.chatView.delete(props.station);
props.api.chat.delete(props.station);
}));
}
@ -145,7 +146,7 @@ export class SettingsScreen extends Component {
awaiting: true,
type: 'Converting chat...'
}, (() => {
props.api.chatView.groupify(
props.api.chat.groupify(
props.station, state.targetGroup, state.inclusive
).then(() => this.setState({ awaiting: false }));
}));
@ -278,7 +279,8 @@ export class SettingsScreen extends Component {
onBlur={() => {
if (chatOwner) {
this.setState({ awaiting: true, type: 'Editing chat...' }, (() => {
props.api.metadata.add(
props.api.metadata.metadataAdd(
'chat',
association['app-path'],
association['group-path'],
state.title,
@ -307,7 +309,8 @@ export class SettingsScreen extends Component {
onBlur={() => {
if (chatOwner) {
this.setState({ awaiting: true, type: 'Editing chat...' }, (() => {
props.api.metadata.add(
props.api.metadata.metadataAdd(
'chat',
association['app-path'],
association['group-path'],
association.metadata.title,

View File

@ -10,50 +10,28 @@ import Tiles from './components/tiles';
import Welcome from './components/welcome';
export default class LaunchApp extends React.Component {
constructor(props) {
super(props);
this.store = new LaunchStore();
this.state = this.store.state;
this.resetControllers();
}
resetControllers() {
this.api = null;
this.subscription = null;
}
componentDidMount() {
document.title = 'OS1 - Home';
// preload spinner asset
new Image().src = '/~landscape/img/Spinner.png';
this.store.setStateHandler(this.setState.bind(this));
const channel = new this.props.channel();
this.api = new LaunchApi(this.props.ship, channel, this.store);
this.subscription = new LaunchSubscription(this.store, this.api, channel);
this.subscription.start();
}
componentWillUnmount() {
this.subscription.delete();
this.store.clear();
this.store.setStateHandler(() => {});
this.resetControllers();
}
componentWillUnmount() {}
render() {
const { state } = this;
const { props } = this;
return (
<div className='v-mid ph2 dtc-m dtc-l dtc-xl flex justify-between flex-wrap' style={{ maxWidth: '40rem' }}>
<Welcome firstTime={state.launch.firstTime} api={this.api} />
<Welcome firstTime={props.launch.firstTime} api={props.api} />
<Tiles
tiles={state.launch.tiles}
tileOrdering={state.launch.tileOrdering}
tiles={props.launch.tiles}
tileOrdering={props.launch.tileOrdering}
api={this.api}
location={state.location}
weather={state.weather}
location={props.location}
weather={props.weather}
/>
</div>
);

View File

@ -1,51 +0,0 @@
import ContactReducer from '../reducers/contact-update';
import ChatReducer from '../reducers/chat-update';
import InviteReducer from '../reducers/invite-update';
import PermissionReducer from '../reducers/permission-update';
import MetadataReducer from '../reducers/metadata-update';
import S3Reducer from '../reducers/s3-update';
import LocalReducer from '../reducers/local';
import BaseStore from './base';
export default class ChatStore extends BaseStore {
constructor() {
super();
this.permissionReducer = new PermissionReducer();
this.contactReducer = new ContactReducer();
this.chatReducer = new ChatReducer();
this.inviteReducer = new InviteReducer();
this.s3Reducer = new S3Reducer();
this.metadataReducer = new MetadataReducer();
this.localReducer = new LocalReducer();
}
initialState() {
return {
inbox: {},
chatSynced: null,
contacts: {},
permissions: {},
invites: {},
associations: {
chat: {},
contacts: {}
},
sidebarShown: true,
pendingMessages: new Map([]),
chatInitialized: false,
s3: {}
};
}
reduce(data, state) {
this.permissionReducer.reduce(data, this.state);
this.contactReducer.reduce(data, this.state);
this.chatReducer.reduce(data, this.state);
this.inviteReducer.reduce(data, this.state);
this.metadataReducer.reduce(data, this.state);
this.localReducer.reduce(data, this.state);
this.s3Reducer.reduce(data, this.state);
}
}