mirror of
https://github.com/ilyakooo0/urbit.git
synced 2025-01-05 22:03:50 +03:00
Merge pull request #3242 from urbit/la/chat-window
interface: refactored chat window into header, window, and scroll container
This commit is contained in:
commit
004bf024db
14
pkg/interface/package-lock.json
generated
14
pkg/interface/package-lock.json
generated
@ -7590,6 +7590,20 @@
|
||||
"memoize-one": ">=3.1.1 <6"
|
||||
}
|
||||
},
|
||||
"react-window-dynamic": {
|
||||
"version": "1.8.0-alpha.2",
|
||||
"resolved": "https://registry.npmjs.org/react-window-dynamic/-/react-window-dynamic-1.8.0-alpha.2.tgz",
|
||||
"integrity": "sha512-PYR1nu5kzEr+PDg9/+19uTqO1OqUCckYE5dDpjFpEGBtVTcq4smxE3jXhk+zLi4AOZlLVr9pJIjwPL68zwq5Ww==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.0.0",
|
||||
"memoize-one": ">=3.1.1 <6"
|
||||
}
|
||||
},
|
||||
"react-window-infinite-loader": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/react-window-infinite-loader/-/react-window-infinite-loader-1.0.5.tgz",
|
||||
"integrity": "sha512-IcPIq8lADK3zsAcqoLqQGyduicqR6jWkiK2VUX5sKSI9X/rou6OWlOEexnGyujdNTG7hSG8OVBFEhLSDs4qrxg=="
|
||||
},
|
||||
"readable-stream": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
|
||||
|
@ -176,5 +176,6 @@ class App extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export default process.env.NODE_ENV === 'production' ? App : hot(App);
|
||||
|
||||
|
@ -1,16 +1,11 @@
|
||||
import React, { Component, Fragment } from "react";
|
||||
import _ from "lodash";
|
||||
import moment from "moment";
|
||||
|
||||
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 { ChatWindow } from './lib/chat-window';
|
||||
import { ChatHeader } from './lib/chat-header';
|
||||
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";
|
||||
@ -21,52 +16,6 @@ import GlobalApi from "../../../api/global";
|
||||
import { Association } from "../../../types/metadata-update";
|
||||
import {Group} from "../../../types/group-update";
|
||||
|
||||
function getNumPending(props: any) {
|
||||
const result = props.pendingMessages.has(props.station)
|
||||
? props.pendingMessages.get(props.station).length
|
||||
: 0;
|
||||
return result;
|
||||
}
|
||||
|
||||
const ACTIVITY_TIMEOUT = 60000; // a minute
|
||||
const DEFAULT_BACKLOG_SIZE = 300;
|
||||
const MAX_BACKLOG_SIZE = 1000;
|
||||
|
||||
function scrollIsAtTop(container) {
|
||||
if (
|
||||
(navigator.userAgent.includes("Safari") &&
|
||||
navigator.userAgent.includes("Chrome")) ||
|
||||
navigator.userAgent.includes("Firefox")
|
||||
) {
|
||||
return container.scrollTop === 0;
|
||||
} else if (navigator.userAgent.includes("Safari")) {
|
||||
return (
|
||||
container.scrollHeight + Math.round(container.scrollTop) <=
|
||||
container.clientHeight + 10
|
||||
);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function scrollIsAtBottom(container) {
|
||||
if (
|
||||
(navigator.userAgent.includes("Safari") &&
|
||||
navigator.userAgent.includes("Chrome")) ||
|
||||
navigator.userAgent.includes("Firefox")
|
||||
) {
|
||||
return (
|
||||
container.scrollHeight - Math.round(container.scrollTop) <=
|
||||
container.clientHeight + 10
|
||||
);
|
||||
} else if (navigator.userAgent.includes("Safari")) {
|
||||
return container.scrollTop === 0;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
type IMessage = Envelope & { pending?: boolean };
|
||||
|
||||
type ChatScreenProps = RouteComponentProps<{
|
||||
ship: Patp;
|
||||
@ -90,47 +39,20 @@ type ChatScreenProps = RouteComponentProps<{
|
||||
};
|
||||
|
||||
interface ChatScreenState {
|
||||
numPages: number;
|
||||
scrollLocked: boolean;
|
||||
read: number;
|
||||
active: boolean;
|
||||
messages: Map<string, string>;
|
||||
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);
|
||||
|
||||
this.state = {
|
||||
numPages: 1,
|
||||
scrollLocked: false,
|
||||
read: props.read,
|
||||
active: true,
|
||||
messages: new Map(),
|
||||
// only for FF
|
||||
lastScrollHeight: null,
|
||||
};
|
||||
|
||||
this.onScroll = this.onScroll.bind(this);
|
||||
|
||||
this.setUnreadMarker = this.setUnreadMarker.bind(this);
|
||||
|
||||
this.handleActivity = this.handleActivity.bind(this);
|
||||
this.setInactive = this.setInactive.bind(this);
|
||||
|
||||
moment.updateLocale("en", {
|
||||
calendar: {
|
||||
sameDay: "[Today]",
|
||||
@ -143,450 +65,67 @@ export class ChatScreen extends Component<ChatScreenProps, ChatScreenState> {
|
||||
});
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
document.addEventListener("mousemove", this.handleActivity, false);
|
||||
document.addEventListener("mousedown", this.handleActivity, false);
|
||||
document.addEventListener("keypress", this.handleActivity, false);
|
||||
document.addEventListener("touchmove", this.handleActivity, false);
|
||||
this.activityTimeout = setTimeout(this.setInactive, ACTIVITY_TIMEOUT);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
document.removeEventListener("mousemove", this.handleActivity, false);
|
||||
document.removeEventListener("mousedown", this.handleActivity, false);
|
||||
document.removeEventListener("keypress", this.handleActivity, false);
|
||||
document.removeEventListener("touchmove", this.handleActivity, false);
|
||||
if (this.activityTimeout) {
|
||||
clearTimeout(this.activityTimeout);
|
||||
}
|
||||
}
|
||||
|
||||
handleActivity() {
|
||||
if (!this.state.active) {
|
||||
this.setState({ active: true });
|
||||
}
|
||||
|
||||
if (this.activityTimeout) {
|
||||
clearTimeout(this.activityTimeout);
|
||||
}
|
||||
|
||||
this.activityTimeout = setTimeout(this.setInactive, ACTIVITY_TIMEOUT);
|
||||
}
|
||||
|
||||
setInactive() {
|
||||
this.activityTimeout = null;
|
||||
this.setState({ active: false, scrollLocked: true });
|
||||
}
|
||||
|
||||
receivedNewChat() {
|
||||
const { props } = this;
|
||||
this.hasAskedForMessages = false;
|
||||
|
||||
this.unreadMarker = null;
|
||||
this.scrolledToMarker = false;
|
||||
|
||||
this.setState({ read: props.read });
|
||||
|
||||
const unread = props.length - props.read;
|
||||
const unreadUnloaded = unread - props.envelopes.length;
|
||||
const excessUnread = unreadUnloaded > MAX_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) {
|
||||
this.scrolledToMarker = true;
|
||||
this.setState(
|
||||
{
|
||||
scrollLocked: false,
|
||||
},
|
||||
() => {
|
||||
this.scrollToBottom();
|
||||
}
|
||||
);
|
||||
} else {
|
||||
this.setState({ scrollLocked: true, numPages: Math.ceil(unread / 100) });
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
const { props, state } = this;
|
||||
|
||||
if (
|
||||
prevProps.match.params.station !== props.match.params.station ||
|
||||
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) {
|
||||
this.hasAskedForMessages = false;
|
||||
} 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) {
|
||||
this.receivedNewChat();
|
||||
}
|
||||
|
||||
if (
|
||||
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")) {
|
||||
this.recalculateScrollTop();
|
||||
}
|
||||
|
||||
this.lastNumPending = getNumPending(props);
|
||||
}
|
||||
}
|
||||
|
||||
askForMessages(size) {
|
||||
const { props, state } = this;
|
||||
|
||||
if (
|
||||
props.envelopes.length >= props.length ||
|
||||
this.hasAskedForMessages ||
|
||||
props.length <= 0
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const start =
|
||||
props.length - props.envelopes[props.envelopes.length - 1].number;
|
||||
if (start > 0) {
|
||||
const end = start + size < props.length ? start + size : props.length;
|
||||
this.hasAskedForMessages = true;
|
||||
props.api.chat.fetchMessages(start + 1, end, props.station);
|
||||
}
|
||||
}
|
||||
|
||||
scrollToBottom() {
|
||||
if (!this.state.scrollLocked && this.scrollElement) {
|
||||
this.scrollElement.scrollIntoView();
|
||||
}
|
||||
}
|
||||
|
||||
// Restore chat position on FF when new messages come in
|
||||
recalculateScrollTop() {
|
||||
const { lastScrollHeight } = this.state;
|
||||
if (!this.scrollContainer || !lastScrollHeight) {
|
||||
return;
|
||||
}
|
||||
|
||||
const target = this.scrollContainer;
|
||||
const newScrollTop = this.scrollContainer.scrollHeight - lastScrollHeight;
|
||||
if (target.scrollTop !== 0 || newScrollTop === target.scrollTop) {
|
||||
return;
|
||||
}
|
||||
target.scrollTop = target.scrollHeight - lastScrollHeight;
|
||||
}
|
||||
|
||||
onScroll(e) {
|
||||
if (scrollIsAtTop(e.target)) {
|
||||
// Save scroll position for FF
|
||||
if (navigator.userAgent.includes("Firefox")) {
|
||||
this.setState({
|
||||
lastScrollHeight: e.target.scrollHeight,
|
||||
});
|
||||
}
|
||||
this.setState(
|
||||
{
|
||||
numPages: this.state.numPages + 1,
|
||||
scrollLocked: true,
|
||||
},
|
||||
() => {
|
||||
this.askForMessages(DEFAULT_BACKLOG_SIZE);
|
||||
}
|
||||
);
|
||||
} else if (scrollIsAtBottom(e.target)) {
|
||||
this.dismissUnread();
|
||||
this.setState({
|
||||
numPages: 1,
|
||||
scrollLocked: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
setUnreadMarker(ref) {
|
||||
if (ref && !this.scrolledToMarker) {
|
||||
this.setState({ scrollLocked: true }, () => {
|
||||
ref.scrollIntoView({ block: "center" });
|
||||
if (ref.offsetParent && scrollIsAtBottom(ref.offsetParent)) {
|
||||
this.dismissUnread();
|
||||
this.setState({
|
||||
numPages: 1,
|
||||
scrollLocked: false,
|
||||
});
|
||||
}
|
||||
});
|
||||
this.scrolledToMarker = true;
|
||||
}
|
||||
this.unreadMarker = ref;
|
||||
}
|
||||
|
||||
dismissUnread() {
|
||||
this.props.api.chat.read(this.props.station);
|
||||
}
|
||||
|
||||
chatWindow(unread) {
|
||||
// Replace with just the "not Firefox" implementation
|
||||
// when Firefox #1042151 is patched.
|
||||
|
||||
const { props, state } = this;
|
||||
|
||||
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: IMessage[] = (
|
||||
props.pendingMessages.get(props.station) || []
|
||||
).map((value) => ({ ...value, pending: true }));
|
||||
|
||||
if(unread !== 0) {
|
||||
unread += pendingMessages.length;
|
||||
}
|
||||
|
||||
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 renderSigil =
|
||||
_.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);
|
||||
|
||||
const when = ["when"];
|
||||
const dayBreak =
|
||||
moment(_.get(messages[i + 1], when)).format("YYYY.MM.DD") !==
|
||||
moment(_.get(messages[i], when)).format("YYYY.MM.DD");
|
||||
|
||||
const messageElem = (
|
||||
<Message
|
||||
key={msg.uid}
|
||||
msg={msg}
|
||||
contacts={props.contacts}
|
||||
renderSigil={renderSigil}
|
||||
paddingTop={paddingTop}
|
||||
paddingBot={paddingBot}
|
||||
pending={Boolean(msg.pending)}
|
||||
group={props.group}
|
||||
association={props.association}
|
||||
/>
|
||||
);
|
||||
if (unread > 0 && i === unread - 1) {
|
||||
return (
|
||||
<Fragment key={msg.uid}>
|
||||
{messageElem}
|
||||
<div
|
||||
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>
|
||||
<hr className="ma0 flex-grow-1 b--green2 bt-0" />
|
||||
{dayBreak && (
|
||||
<p className="gray2 mh4">
|
||||
{moment(_.get(messages[i], when)).calendar()}
|
||||
</p>
|
||||
)}
|
||||
<hr
|
||||
style={{ width: "calc(50% - 48px)" }}
|
||||
className="b--green2 ma0 bt-0"
|
||||
/>
|
||||
</div>
|
||||
</Fragment>
|
||||
);
|
||||
} else if (dayBreak) {
|
||||
return (
|
||||
<Fragment key={msg.uid}>
|
||||
{messageElem}
|
||||
<div
|
||||
className="pv3 gray2 b--gray2 flex items-center justify-center f9 "
|
||||
>
|
||||
<p>{moment(_.get(messages[i], when)).calendar()}</p>
|
||||
</div>
|
||||
</Fragment>
|
||||
);
|
||||
} else {
|
||||
return messageElem;
|
||||
}
|
||||
});
|
||||
|
||||
if (navigator.userAgent.includes("Firefox")) {
|
||||
return (
|
||||
<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" }}
|
||||
>
|
||||
<div
|
||||
ref={(el) => {
|
||||
this.scrollElement = el;
|
||||
}}
|
||||
></div>
|
||||
{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 />
|
||||
)}
|
||||
{messageElements}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div
|
||||
className="overflow-y-scroll bg-white bg-gray0-d pt3 pb2 flex flex-column-reverse relative"
|
||||
style={{ height: "100%", resize: "vertical" }}
|
||||
onScroll={this.onScroll}
|
||||
>
|
||||
<div
|
||||
ref={(el) => {
|
||||
this.scrollElement = el;
|
||||
}}
|
||||
></div>
|
||||
{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 />
|
||||
)}
|
||||
{messageElements}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { props, state } = this;
|
||||
|
||||
const messages = props.envelopes.slice(0);
|
||||
|
||||
const lastMsgNum = messages.length > 0 ? messages.length : 0;
|
||||
|
||||
const group = Array.from(props.group.members);
|
||||
|
||||
const isinPopout = props.popout ? "popout/" : "";
|
||||
|
||||
const lastMsgNum = props.envelopes.length > 0 ? props.envelopes.length : 0;
|
||||
const ownerContact =
|
||||
window.ship in props.contacts ? props.contacts[window.ship] : false;
|
||||
|
||||
let title = props.station.substr(1);
|
||||
const pendingMessages = (props.pendingMessages.get(props.station) || [])
|
||||
.map((value) => ({
|
||||
...value,
|
||||
pending: true
|
||||
}));
|
||||
|
||||
if (props.association && "metadata" in props.association) {
|
||||
title =
|
||||
props.association.metadata.title !== ""
|
||||
? props.association.metadata.title
|
||||
: props.station.substr(1);
|
||||
}
|
||||
const isChatMissing =
|
||||
props.chatInitialized &&
|
||||
!(props.station in props.inbox) &&
|
||||
props.chatSynced &&
|
||||
!(props.station in props.chatSynced);
|
||||
|
||||
const unread = props.length - state.read;
|
||||
const isChatLoading =
|
||||
props.chatInitialized &&
|
||||
!(props.station in props.inbox) &&
|
||||
props.chatSynced &&
|
||||
(props.station in props.chatSynced);
|
||||
|
||||
const unreadMsg = unread > 0 && messages[unread - 1];
|
||||
|
||||
const showUnreadNotice =
|
||||
props.length !== props.read && props.read === state.read;
|
||||
const isChatUnsynced =
|
||||
props.chatSynced &&
|
||||
!(props.station in props.chatSynced) &&
|
||||
props.envelopes.length > 0;
|
||||
|
||||
const unreadCount = props.length - props.read;
|
||||
const unreadMsg = unreadCount > 0 && props.envelopes[unreadCount - 1];
|
||||
|
||||
return (
|
||||
<div
|
||||
key={props.station}
|
||||
className="h-100 w-100 overflow-hidden flex flex-column relative"
|
||||
>
|
||||
<div
|
||||
className="w-100 dn-m dn-l dn-xl inter pt4 pb6 pl3 f8"
|
||||
style={{ height: "1rem" }}
|
||||
>
|
||||
<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 "
|
||||
}
|
||||
style={{ height: 48 }}
|
||||
>
|
||||
<SidebarSwitcher
|
||||
sidebarShown={props.sidebarShown}
|
||||
popout={props.popout}
|
||||
api={props.api}
|
||||
/>
|
||||
<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" }}
|
||||
>
|
||||
{title}
|
||||
</h2>
|
||||
</Link>
|
||||
<ChatTabBar
|
||||
{...props}
|
||||
station={props.station}
|
||||
numPeers={group.length}
|
||||
isOwner={deSig(props.match.params.ship) === window.ship}
|
||||
popout={props.popout}
|
||||
api={props.api}
|
||||
/>
|
||||
</div>
|
||||
{!!unreadMsg && showUnreadNotice && (
|
||||
<UnreadNotice
|
||||
unread={unread}
|
||||
unreadMsg={unreadMsg}
|
||||
onRead={() => this.dismissUnread()}
|
||||
/>
|
||||
)}
|
||||
{this.chatWindow(unread)}
|
||||
className="h-100 w-100 overflow-hidden flex flex-column relative">
|
||||
<ChatHeader
|
||||
match={props.match}
|
||||
location={props.location}
|
||||
api={props.api}
|
||||
group={props.group}
|
||||
association={props.association}
|
||||
station={props.station}
|
||||
sidebarShown={props.sidebarShown}
|
||||
popout={props.popout} />
|
||||
<ChatWindow
|
||||
isChatMissing={isChatMissing}
|
||||
isChatLoading={isChatLoading}
|
||||
isChatUnsynced={isChatUnsynced}
|
||||
unreadCount={unreadCount}
|
||||
unreadMsg={unreadMsg}
|
||||
pendingMessages={pendingMessages}
|
||||
messages={props.envelopes}
|
||||
length={props.length}
|
||||
contacts={props.contacts}
|
||||
association={props.association}
|
||||
group={props.group}
|
||||
ship={props.match.params.ship}
|
||||
station={props.station}
|
||||
api={props.api} />
|
||||
<ChatInput
|
||||
api={props.api}
|
||||
numMsgs={lastMsgNum}
|
||||
@ -595,7 +134,6 @@ export class ChatScreen extends Component<ChatScreenProps, ChatScreenState> {
|
||||
ownerContact={ownerContact}
|
||||
envelopes={props.envelopes}
|
||||
contacts={props.contacts}
|
||||
onEnter={() => this.setState({ scrollLocked: false })}
|
||||
onUnmount={(msg: string) => this.setState({
|
||||
messages: this.state.messages.set(props.station, msg)
|
||||
})}
|
||||
|
@ -1,21 +1,22 @@
|
||||
import React, { Component } from 'react';
|
||||
|
||||
export class BacklogElement extends Component {
|
||||
render() {
|
||||
return (
|
||||
<div className="center mw6">
|
||||
<div className="db pa3 ma3 ba b--gray4 bg-gray5 b--gray2-d bg-gray1-d white-d flex items-center">
|
||||
<img className="invert-d spin-active v-mid"
|
||||
src="/~chat/img/Spinner.png"
|
||||
width={16}
|
||||
height={16}
|
||||
/>
|
||||
<p className="lh-copy db ml3">
|
||||
Past messages are being restored
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
);
|
||||
export const BacklogElement = (props) => {
|
||||
if (!props.isChatLoading) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div className="center mw6">
|
||||
<div className={
|
||||
"db pa3 ma3 ba b--gray4 bg-gray5 b--gray2-d bg-gray1-d " +
|
||||
"white-d flex items-center"
|
||||
}>
|
||||
<img className="invert-d spin-active v-mid"
|
||||
src="/~chat/img/Spinner.png"
|
||||
width={16}
|
||||
height={16}
|
||||
/>
|
||||
<p className="lh-copy db ml3">Past messages are being restored</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
65
pkg/interface/src/apps/chat/components/lib/chat-header.js
Normal file
65
pkg/interface/src/apps/chat/components/lib/chat-header.js
Normal file
@ -0,0 +1,65 @@
|
||||
import React, { Component, Fragment } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
import { ChatTabBar } from "./chat-tabbar";
|
||||
import { SidebarSwitcher } from "../../../../components/SidebarSwitch";
|
||||
import { deSig } from "../../../../lib/util";
|
||||
|
||||
|
||||
export class ChatHeader extends Component {
|
||||
|
||||
render() {
|
||||
const { props } = this;
|
||||
const isinPopout = props.popout ? "popout/" : "";
|
||||
const group = Array.from(props.group.members);
|
||||
let title = props.station.substr(1);
|
||||
if (props.association &&
|
||||
"metadata" in props.association &&
|
||||
props.association.metadata.tile !== "") {
|
||||
title = props.association.metadata.title
|
||||
}
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<div
|
||||
className="w-100 dn-m dn-l dn-xl inter pt4 pb6 pl3 f8"
|
||||
style={{ height: "1rem" }}>
|
||||
<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 "
|
||||
}
|
||||
style={{ height: 48 }}
|
||||
>
|
||||
<SidebarSwitcher
|
||||
sidebarShown={props.sidebarShown}
|
||||
popout={props.popout}
|
||||
api={props.api}
|
||||
/>
|
||||
<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" }}
|
||||
>
|
||||
{title}
|
||||
</h2>
|
||||
</Link>
|
||||
<ChatTabBar
|
||||
location={props.location}
|
||||
station={props.station}
|
||||
isOwner={deSig(props.match.params.ship) === window.ship}
|
||||
popout={props.popout}
|
||||
/>
|
||||
</div>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
}
|
@ -181,7 +181,7 @@ export class ChatInput extends Component {
|
||||
|
||||
return (
|
||||
<div className={
|
||||
"pa3 cf flex black white-d bt b--gray4 b--gray1-d bg-white" +
|
||||
"pa3 cf flex black white-d bt b--gray4 b--gray1-d bg-white " +
|
||||
"bg-gray0-d relative"
|
||||
}
|
||||
style={{ flexGrow: 1 }}>
|
||||
|
84
pkg/interface/src/apps/chat/components/lib/chat-message.tsx
Normal file
84
pkg/interface/src/apps/chat/components/lib/chat-message.tsx
Normal file
@ -0,0 +1,84 @@
|
||||
import React, { PureComponent, Fragment } from "react";
|
||||
import moment from "moment";
|
||||
|
||||
import { Message } from "./message";
|
||||
|
||||
type IMessage = Envelope & { pending?: boolean };
|
||||
|
||||
|
||||
export const ChatMessage = (props) => {
|
||||
const {
|
||||
msg,
|
||||
previousMsg,
|
||||
nextMsg,
|
||||
isLastUnread,
|
||||
group,
|
||||
association,
|
||||
contacts,
|
||||
unreadRef
|
||||
} = props;
|
||||
|
||||
// Render sigil if previous message is not by the same sender
|
||||
const aut = ["author"];
|
||||
const renderSigil =
|
||||
_.get(nextMsg, aut) !== _.get(msg, aut, msg.author);
|
||||
const paddingTop = renderSigil;
|
||||
const paddingBot =
|
||||
_.get(previousMsg, aut) !== _.get(msg, aut, msg.author);
|
||||
|
||||
const when = ["when"];
|
||||
const dayBreak =
|
||||
moment(_.get(nextMsg, when)).format("YYYY.MM.DD") !==
|
||||
moment(_.get(msg, when)).format("YYYY.MM.DD");
|
||||
|
||||
const messageElem = (
|
||||
<Message
|
||||
key={msg.uid}
|
||||
msg={msg}
|
||||
renderSigil={renderSigil}
|
||||
paddingTop={paddingTop}
|
||||
paddingBot={paddingBot}
|
||||
pending={Boolean(msg.pending)}
|
||||
group={group}
|
||||
contacts={contacts}
|
||||
association={association}
|
||||
/>
|
||||
);
|
||||
|
||||
if (props.isLastUnread) {
|
||||
return (
|
||||
<Fragment key={msg.uid}>
|
||||
{messageElem}
|
||||
<div ref={unreadRef}
|
||||
className="mv2 green2 flex items-center f9">
|
||||
<hr className="dn-s ma0 w2 b--green2 bt-0" />
|
||||
<p className="mh4">New messages below</p>
|
||||
<hr className="ma0 flex-grow-1 b--green2 bt-0" />
|
||||
{dayBreak && (
|
||||
<p className="gray2 mh4">
|
||||
{moment(_.get(msg, when)).calendar()}
|
||||
</p>
|
||||
)}
|
||||
<hr
|
||||
style={{ width: "calc(50% - 48px)" }}
|
||||
className="b--green2 ma0 bt-0"
|
||||
/>
|
||||
</div>
|
||||
</Fragment>
|
||||
);
|
||||
} else if (dayBreak) {
|
||||
return (
|
||||
<Fragment key={msg.uid}>
|
||||
{messageElem}
|
||||
<div
|
||||
className="pv3 gray2 b--gray2 flex items-center justify-center f9 "
|
||||
>
|
||||
<p>{moment(_.get(msg, when)).calendar()}</p>
|
||||
</div>
|
||||
</Fragment>
|
||||
);
|
||||
} else {
|
||||
return messageElem;
|
||||
}
|
||||
};
|
||||
|
@ -0,0 +1,143 @@
|
||||
import React, { Component, Fragment } from "react";
|
||||
|
||||
import { scrollIsAtTop, scrollIsAtBottom } from "../../../../lib/util";
|
||||
|
||||
// Restore chat position on FF when new messages come in
|
||||
const recalculateScrollTop = (lastScrollHeight, scrollContainer) => {
|
||||
if (!scrollContainer || !lastScrollHeight) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newScrollTop = scrollContainer.scrollHeight - lastScrollHeight;
|
||||
if (scrollContainer.scrollTop !== 0 ||
|
||||
scrollContainer.scrollTop === newScrollTop) {
|
||||
return;
|
||||
}
|
||||
|
||||
scrollContainer.scrollTop = scrollContainer.scrollHeight - lastScrollHeight;
|
||||
};
|
||||
|
||||
|
||||
export class ChatScrollContainer extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
// only for FF
|
||||
this.state = {
|
||||
lastScrollHeight: null
|
||||
};
|
||||
|
||||
this.isTriggeredScroll = false;
|
||||
|
||||
this.isAtBottom = true;
|
||||
this.isAtTop = false;
|
||||
|
||||
this.containerDidScroll = this.containerDidScroll.bind(this);
|
||||
|
||||
this.containerRef = React.createRef();
|
||||
this.scrollRef = React.createRef();
|
||||
}
|
||||
|
||||
containerDidScroll(e) {
|
||||
const { props } = this;
|
||||
if (scrollIsAtTop(e.target)) {
|
||||
// Save scroll position for FF
|
||||
if (navigator.userAgent.includes("Firefox")) {
|
||||
this.setState({
|
||||
lastScrollHeight: e.target.scrollHeight,
|
||||
});
|
||||
}
|
||||
|
||||
if (!this.isAtTop) {
|
||||
props.scrollIsAtTop();
|
||||
}
|
||||
|
||||
this.isTriggeredScroll = false;
|
||||
this.isAtBottom = false;
|
||||
this.isAtTop = true;
|
||||
} else if (scrollIsAtBottom(e.target) && !this.isTriggeredScroll) {
|
||||
if (!this.isAtBottom) {
|
||||
props.scrollIsAtBottom();
|
||||
}
|
||||
|
||||
this.isTriggeredScroll = false;
|
||||
this.isAtBottom = true;
|
||||
this.isAtTop = false;
|
||||
} else {
|
||||
this.isAtBottom = false;
|
||||
this.isAtTop = false;
|
||||
this.isTriggeredScroll = false;
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
// Replace with just the "not Firefox" implementation
|
||||
// when Firefox #1042151 is patched.
|
||||
|
||||
if (navigator.userAgent.includes("Firefox")) {
|
||||
return this.firefoxScrollContainer();
|
||||
} else {
|
||||
return this.normalScrollContainer();
|
||||
}
|
||||
}
|
||||
|
||||
firefoxScrollContainer() {
|
||||
return (
|
||||
<div
|
||||
className="relative overflow-y-scroll h-100"
|
||||
onScroll={this.containerDidScroll}
|
||||
ref={this.containerRef}>
|
||||
<div
|
||||
className="bg-white bg-gray0-d pt3 pb2 flex flex-column-reverse"
|
||||
style={{ resize: "vertical" }}>
|
||||
<div ref={this.scrollRef}></div>
|
||||
{this.props.children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
normalScrollContainer() {
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
"overflow-y-scroll bg-white bg-gray0-d pt3 pb2 flex " +
|
||||
"flex-column-reverse relative"
|
||||
}
|
||||
style={{ height: "100%", resize: "vertical" }}
|
||||
onScroll={this.containerDidScroll}>
|
||||
<div ref={this.scrollRef}></div>
|
||||
{this.props.children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
scrollToBottom() {
|
||||
this.isTriggeredScroll = true;
|
||||
if (this.scrollRef.current) {
|
||||
this.scrollRef.current.scrollIntoView(false);
|
||||
}
|
||||
|
||||
if (navigator.userAgent.includes("Firefox")) {
|
||||
recalculateScrollTop(
|
||||
this.state.lastScrollHeight,
|
||||
this.scrollContainer
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
scrollToReference(ref) {
|
||||
this.isTriggeredScroll = true;
|
||||
if (this.scrollRef.current && ref.current) {
|
||||
ref.current.scrollIntoView({ block: 'center' });
|
||||
}
|
||||
|
||||
if (navigator.userAgent.includes("Firefox")) {
|
||||
recalculateScrollTop(
|
||||
this.state.lastScrollHeight,
|
||||
this.scrollContainer
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
191
pkg/interface/src/apps/chat/components/lib/chat-window.tsx
Normal file
191
pkg/interface/src/apps/chat/components/lib/chat-window.tsx
Normal file
@ -0,0 +1,191 @@
|
||||
import React, { Component, Fragment } from "react";
|
||||
|
||||
import { ChatMessage } from './chat-message';
|
||||
import { ChatScrollContainer } from "./chat-scroll-container";
|
||||
import { UnreadNotice } from "./unread-notice";
|
||||
import { ResubscribeElement } from "./resubscribe-element";
|
||||
import { BacklogElement } from "./backlog-element";
|
||||
|
||||
const MAX_BACKLOG_SIZE = 1000;
|
||||
const DEFAULT_BACKLOG_SIZE = 200;
|
||||
const PAGE_SIZE = 50;
|
||||
const INITIAL_LOAD = 20;
|
||||
|
||||
|
||||
export class ChatWindow extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
numPages: 1,
|
||||
};
|
||||
|
||||
this.hasAskedForMessages = false;
|
||||
|
||||
this.dismissUnread = this.dismissUnread.bind(this);
|
||||
this.scrollIsAtBottom = this.scrollIsAtBottom.bind(this);
|
||||
this.scrollIsAtTop = this.scrollIsAtTop.bind(this);
|
||||
|
||||
this.scrollReference = React.createRef();
|
||||
this.unreadReference = React.createRef();
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.initialFetch();
|
||||
|
||||
if (this.state.numPages === 1 && this.props.unreadCount < INITIAL_LOAD) {
|
||||
this.dismissUnread();
|
||||
this.scrollToBottom();
|
||||
}
|
||||
}
|
||||
|
||||
initialFetch() {
|
||||
const { props } = this;
|
||||
if (props.messages.length > 0) {
|
||||
const unreadUnloaded = props.unreadCount - props.messages.length;
|
||||
|
||||
if (unreadUnloaded <= MAX_BACKLOG_SIZE &&
|
||||
unreadUnloaded + INITIAL_LOAD > DEFAULT_BACKLOG_SIZE) {
|
||||
this.fetchBacklog(unreadUnloaded + INITIAL_LOAD);
|
||||
} else {
|
||||
this.fetchBacklog(DEFAULT_BACKLOG_SIZE);
|
||||
}
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
this.initialFetch();
|
||||
}, 2000);
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
const { props, state } = this;
|
||||
|
||||
if (props.isChatMissing) {
|
||||
props.history.push("/~chat");
|
||||
} else if (props.messages.length >= prevProps.messages.length + 10) {
|
||||
this.hasAskedForMessages = false;
|
||||
let numPages = props.unreadCount > 0 ?
|
||||
Math.ceil(props.unreadCount / PAGE_SIZE) : this.state.numPages;
|
||||
|
||||
if (this.state.numPages === numPages) {
|
||||
if (props.unreadCount > 20) {
|
||||
this.scrollToUnread();
|
||||
}
|
||||
} else {
|
||||
this.setState({ numPages }, () => {
|
||||
if (props.unreadCount > 20) {
|
||||
this.scrollToUnread();
|
||||
}
|
||||
});
|
||||
}
|
||||
} else if (
|
||||
state.numPages === 1 &&
|
||||
this.props.unreadCount < INITIAL_LOAD &&
|
||||
this.props.unreadCount > 0
|
||||
) {
|
||||
this.dismissUnread();
|
||||
this.scrollToBottom();
|
||||
}
|
||||
}
|
||||
|
||||
scrollIsAtTop() {
|
||||
const { props, state } = this;
|
||||
this.setState({ numPages: state.numPages + 1 }, () => {
|
||||
if (state.numPages * PAGE_SIZE < props.length) {
|
||||
this.fetchBacklog(DEFAULT_BACKLOG_SIZE);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
scrollIsAtBottom() {
|
||||
if (this.state.numPages !== 1) {
|
||||
this.setState({ numPages: 1 });
|
||||
}
|
||||
}
|
||||
|
||||
scrollToBottom() {
|
||||
if (this.scrollReference.current) {
|
||||
this.scrollReference.current.scrollToBottom();
|
||||
}
|
||||
if (this.state.numPages !== 1) {
|
||||
this.setState({ numPages: 1 });
|
||||
}
|
||||
}
|
||||
|
||||
scrollToUnread() {
|
||||
if (this.scrollReference.current && this.unreadReference.current) {
|
||||
this.scrollReference.current.scrollToReference(this.unreadReference);
|
||||
}
|
||||
}
|
||||
|
||||
dismissUnread() {
|
||||
this.props.api.chat.read(this.props.station);
|
||||
}
|
||||
|
||||
fetchBacklog(size) {
|
||||
const { props } = this;
|
||||
|
||||
if (
|
||||
props.messages.length >= props.length ||
|
||||
this.hasAskedForMessages ||
|
||||
props.length <= 0
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const start =
|
||||
props.length - props.messages[props.messages.length - 1].number;
|
||||
if (start > 0) {
|
||||
const end = start + size < props.length ? start + size : props.length;
|
||||
props.api.chat.fetchMessages(start + 1, end, props.station);
|
||||
this.hasAskedForMessages = true;
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { props, state } = this;
|
||||
const sliceLength = Math.min(
|
||||
state.numPages * PAGE_SIZE,
|
||||
props.messages.length + props.pendingMessages.length
|
||||
);
|
||||
const messages =
|
||||
props.pendingMessages
|
||||
.concat(props.messages)
|
||||
.slice(0, sliceLength);
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<UnreadNotice
|
||||
unreadCount={props.unreadCount}
|
||||
unreadMsg={props.unreadMsg}
|
||||
dismissUnread={this.dismissUnread} />
|
||||
<ChatScrollContainer
|
||||
ref={this.scrollReference}
|
||||
scrollIsAtBottom={this.scrollIsAtBottom}
|
||||
scrollIsAtTop={this.scrollIsAtTop}>
|
||||
<BacklogElement isChatLoading={props.isChatLoading} />
|
||||
<ResubscribeElement
|
||||
api={props.api}
|
||||
host={props.ship}
|
||||
station={props.station}
|
||||
isChatUnsynced={props.isChatUnsynced}
|
||||
/>
|
||||
{ messages.map((msg, i) => (
|
||||
<ChatMessage
|
||||
unreadRef={this.unreadReference}
|
||||
isLastUnread={
|
||||
props.unreadCount > 0 && i === props.unreadCount - 1
|
||||
}
|
||||
msg={msg}
|
||||
previousMsg={messages[i - 1]}
|
||||
nextMsg={messages[i + 1]}
|
||||
association={props.association}
|
||||
group={props.group}
|
||||
contacts={props.contacts} />
|
||||
))
|
||||
}
|
||||
</ChatScrollContainer>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
import React, { Component } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import RemarkDisableTokenizers from 'remark-disable-tokenizers';
|
||||
import urbitOb from 'urbit-ob';
|
||||
|
@ -5,49 +5,41 @@ import { uxToHex, cite, writeText } from '../../../../lib/util';
|
||||
import moment from 'moment';
|
||||
|
||||
|
||||
export class Message extends Component {
|
||||
constructor() {
|
||||
super();
|
||||
this.state = {
|
||||
copied: false
|
||||
};
|
||||
}
|
||||
export const Message = (props) => {
|
||||
const pending = props.msg.pending ? ' o-40' : '';
|
||||
const containerClass =
|
||||
props.renderSigil ?
|
||||
`w-100 f7 pl3 pt4 pr3 cf flex lh-copy ` + pending :
|
||||
'w-100 pr3 cf hide-child flex' + pending;
|
||||
|
||||
render() {
|
||||
const { props, state } = this;
|
||||
|
||||
const pending = props.msg.pending ? ' o-40' : '';
|
||||
const containerClass =
|
||||
props.renderSigil ?
|
||||
`w-100 f7 pl3 pt4 pr3 cf flex lh-copy ` + pending :
|
||||
'w-100 pr3 cf hide-child flex' + pending;
|
||||
|
||||
const timestamp =
|
||||
moment.unix(props.msg.when / 1000).format(
|
||||
props.renderSigil ? 'hh:mm a' : 'hh:mm'
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={this.containerRef}
|
||||
className={containerClass}
|
||||
style={{
|
||||
minHeight: 'min-content'
|
||||
}}
|
||||
>
|
||||
{
|
||||
props.renderSigil ? (
|
||||
this.renderWithSigil(timestamp)
|
||||
) : (
|
||||
this.renderWithoutSigil(timestamp)
|
||||
)
|
||||
}
|
||||
</div>
|
||||
const timestamp =
|
||||
moment.unix(props.msg.when / 1000).format(
|
||||
props.renderSigil ? 'hh:mm a' : 'hh:mm'
|
||||
);
|
||||
}
|
||||
|
||||
renderWithSigil(timestamp) {
|
||||
const { props, state } = this;
|
||||
return (
|
||||
<div className={containerClass}
|
||||
style={{
|
||||
minHeight: 'min-content'
|
||||
}}>
|
||||
{
|
||||
props.renderSigil ? (
|
||||
renderWithSigil(props, timestamp)
|
||||
) : (
|
||||
<div className="flex w-100">
|
||||
<p className="child pt2 pl2 pr1 mono f9 gray2 dib">{timestamp}</p>
|
||||
<div className="fr f7 clamp-message white-d pr3 lh-copy"
|
||||
style={{ flexGrow: 1 }}>
|
||||
<MessageContent letter={props.msg.letter} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderWithSigil = (props, timestamp) => {
|
||||
const paddingTop = props.paddingTop ? { 'paddingTop': '6px' } : '';
|
||||
const datestamp =
|
||||
'~' + moment.unix(props.msg.when / 1000).format('YYYY.M.D');
|
||||
@ -86,18 +78,14 @@ export class Message extends Component {
|
||||
<span
|
||||
className={
|
||||
'mw5 dib truncate pointer ' +
|
||||
(contact.nickname || state.copied ? '' : 'mono')
|
||||
(contact.nickname ? '' : 'mono')
|
||||
}
|
||||
onClick={() => {
|
||||
writeText(props.msg.author);
|
||||
this.setState({ copied: true });
|
||||
setTimeout(() => {
|
||||
this.setState({ copied: false });
|
||||
}, 800);
|
||||
}}
|
||||
title={`~${props.msg.author}`}
|
||||
>
|
||||
{state.copied && 'Copied' || name}
|
||||
{name}
|
||||
</span>
|
||||
</p>
|
||||
<p className={`v-mid mono f9 gray2 dib`}>{timestamp}</p>
|
||||
@ -111,17 +99,3 @@ export class Message extends Component {
|
||||
);
|
||||
}
|
||||
|
||||
renderWithoutSigil(timestamp) {
|
||||
const { props } = this;
|
||||
|
||||
return (
|
||||
<div className="flex w-100">
|
||||
<p className="child pt2 pl2 pr1 mono f9 gray2 dib">{timestamp}</p>
|
||||
<div className="fr f7 clamp-message white-d pr3 lh-copy"
|
||||
style={{ flexGrow: 1 }}>
|
||||
<MessageContent letter={props.msg.letter} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -9,19 +9,24 @@ export class ResubscribeElement extends Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="db pa3 ma3 ba b--yellow2 bg-yellow0">
|
||||
<p className="lh-copy db">
|
||||
Your ship has been disconnected from the chat's host.
|
||||
This may be due to a bad connection, going offline, lack of permission,
|
||||
or an over-the-air update.
|
||||
</p>
|
||||
<a onClick={this.onClickResubscribe.bind(this)}
|
||||
className="db underline black pointer mt3"
|
||||
>
|
||||
Reconnect to this chat
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
const { props } = this;
|
||||
if (props.isChatUnsynced) {
|
||||
return (
|
||||
<div className="db pa3 ma3 ba b--yellow2 bg-yellow0">
|
||||
<p className="lh-copy db">
|
||||
Your ship has been disconnected from the chat's host.
|
||||
This may be due to a bad connection, going offline, lack of permission,
|
||||
or an over-the-air update.
|
||||
</p>
|
||||
<a onClick={this.onClickResubscribe.bind(this)}
|
||||
className="db underline black pointer mt3"
|
||||
>
|
||||
Reconnect to this chat
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,37 +1,41 @@
|
||||
import React, { Component } from 'react';
|
||||
import moment from 'moment';
|
||||
|
||||
export class UnreadNotice extends Component {
|
||||
render() {
|
||||
const { unread, unreadMsg, onRead } = this.props;
|
||||
export const UnreadNotice = (props) => {
|
||||
const { unreadCount, unreadMsg, dismissUnread } = props;
|
||||
|
||||
let datestamp = moment.unix(unreadMsg.when / 1000).format('YYYY.M.D');
|
||||
const timestamp = moment.unix(unreadMsg.when / 1000).format('HH:mm');
|
||||
if (!unreadMsg || (unreadCount === 0)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (datestamp === moment().format('YYYY.M.D')) {
|
||||
datestamp = null;
|
||||
}
|
||||
let datestamp = moment.unix(unreadMsg.when / 1000).format('YYYY.M.D');
|
||||
const timestamp = moment.unix(unreadMsg.when / 1000).format('HH:mm');
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{ left: '0px' }}
|
||||
className="pa4 w-100 absolute z-1 unread-notice"
|
||||
>
|
||||
<div className="ba b--green2 green2 bg-white bg-gray0-d flex items-center pa2 f9 justify-between br1">
|
||||
<p className="lh-copy db">
|
||||
{unread} new messages since{' '}
|
||||
{datestamp && (
|
||||
<>
|
||||
<span className="green3">~{datestamp}</span> at{' '}
|
||||
</>
|
||||
)}
|
||||
<span className="green3">{timestamp}</span>
|
||||
</p>
|
||||
<div onClick={onRead} className="ml4 inter b--green2 pointer tr lh-copy">
|
||||
Mark as Read
|
||||
</div>
|
||||
if (datestamp === moment().format('YYYY.M.D')) {
|
||||
datestamp = null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ left: '0px' }}
|
||||
className="pa4 w-100 absolute z-1 unread-notice">
|
||||
<div className={
|
||||
"ba b--green2 green2 bg-white bg-gray0-d flex items-center " +
|
||||
"pa2 f9 justify-between br1"
|
||||
}>
|
||||
<p className="lh-copy db">
|
||||
{unreadCount} new messages since{' '}
|
||||
{datestamp && (
|
||||
<>
|
||||
<span className="green3">~{datestamp}</span> at{' '}
|
||||
</>
|
||||
)}
|
||||
<span className="green3">{timestamp}</span>
|
||||
</p>
|
||||
<div onClick={dismissUnread}
|
||||
className="ml4 inter b--green2 pointer tr lh-copy">
|
||||
Mark as Read
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -275,3 +275,38 @@ export function stringToSymbol(str) {
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export function scrollIsAtTop(container) {
|
||||
if (
|
||||
(navigator.userAgent.includes("Safari") &&
|
||||
navigator.userAgent.includes("Chrome")) ||
|
||||
navigator.userAgent.includes("Firefox")
|
||||
) {
|
||||
return container.scrollTop === 0;
|
||||
} else if (navigator.userAgent.includes("Safari")) {
|
||||
return (
|
||||
container.scrollHeight + Math.round(container.scrollTop) <=
|
||||
container.clientHeight + 10
|
||||
);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function scrollIsAtBottom(container) {
|
||||
if (
|
||||
(navigator.userAgent.includes("Safari") &&
|
||||
navigator.userAgent.includes("Chrome")) ||
|
||||
navigator.userAgent.includes("Firefox")
|
||||
) {
|
||||
return (
|
||||
container.scrollHeight - Math.round(container.scrollTop) <=
|
||||
container.clientHeight + 10
|
||||
);
|
||||
} else if (navigator.userAgent.includes("Safari")) {
|
||||
return container.scrollTop === 0;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -50,7 +50,6 @@ export default class LaunchReducer<S extends LaunchState> {
|
||||
|
||||
changeIsShown(json: LaunchUpdate, state: S) {
|
||||
const data = _.get(json, 'changeIsShown', false);
|
||||
console.log(json, data);
|
||||
if (data) {
|
||||
let tile = state.launch.tiles[data.name];
|
||||
console.log(tile);
|
||||
|
Loading…
Reference in New Issue
Block a user