mirror of
https://github.com/ilyakooo0/urbit.git
synced 2024-11-10 10:05:09 +03:00
Merge branch 'release/next-js' into release/hot/2020-1-11
This commit is contained in:
commit
c6069bda3f
@ -1,7 +1,7 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<title>OS1</title>
|
||||
<title>Landscape</title>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport"
|
||||
content="width=device-width, initial-scale=1, shrink-to-fit=no,maximum-scale=1"/>
|
||||
@ -12,8 +12,8 @@
|
||||
<link rel="icon" type="image/png" href="/~landscape/img/Favicon.png">
|
||||
<link rel="manifest"
|
||||
href='data:application/manifest+json,{
|
||||
"name": "OS1",
|
||||
"short_name": "OS1",
|
||||
"name": "Landscape",
|
||||
"short_name": "Landscape",
|
||||
"description": "An%20interface%20to%20your%20Urbit.",
|
||||
"display": "standalone",
|
||||
"background_color": "%23FFFFFF",
|
||||
|
@ -2,6 +2,7 @@ const path = require('path');
|
||||
// const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
|
||||
const MomentLocalesPlugin = require('moment-locales-webpack-plugin');
|
||||
const webpack = require('webpack');
|
||||
|
||||
module.exports = {
|
||||
mode: 'production',
|
||||
@ -53,6 +54,10 @@ module.exports = {
|
||||
plugins: [
|
||||
new MomentLocalesPlugin(),
|
||||
new CleanWebpackPlugin(),
|
||||
new webpack.DefinePlugin({
|
||||
'process.env.LANDSCAPE_STREAM': JSON.stringify(process.env.LANDSCAPE_STREAM),
|
||||
'process.env.LANDSCAPE_SHORTHASH': JSON.stringify(process.env.LANDSCAPE_SHORTHASH)
|
||||
})
|
||||
// new HtmlWebpackPlugin({
|
||||
// title: 'Hot Module Replacement',
|
||||
// template: './public/index.html',
|
||||
@ -61,7 +66,7 @@ module.exports = {
|
||||
output: {
|
||||
filename: 'index.[contenthash].js',
|
||||
path: path.resolve(__dirname, '../../arvo/app/landscape/js/bundle'),
|
||||
publicPath: '/',
|
||||
publicPath: '/'
|
||||
},
|
||||
optimization: {
|
||||
minimize: true,
|
||||
|
10
pkg/interface/package-lock.json
generated
10
pkg/interface/package-lock.json
generated
@ -5681,6 +5681,11 @@
|
||||
"integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==",
|
||||
"dev": true
|
||||
},
|
||||
"immer": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/immer/-/immer-8.0.0.tgz",
|
||||
"integrity": "sha512-jm87NNBAIG4fHwouilCHIecFXp5rMGkiFrAuhVO685UnMAlOneEAnOyzPt8OnP47TC11q/E7vpzZe0WvwepFTg=="
|
||||
},
|
||||
"import-fresh": {
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.2.1.tgz",
|
||||
@ -12212,6 +12217,11 @@
|
||||
"synchronous-promise": "^2.0.13",
|
||||
"toposort": "^2.0.2"
|
||||
}
|
||||
},
|
||||
"zustand": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/zustand/-/zustand-3.2.0.tgz",
|
||||
"integrity": "sha512-MBYFrnUdgFVi38tdQNSzVN9cPpRDf7w2HhdHGDSgBRHN7vIbUGUR3aBdVQykelXzSFR7iVj3YNBuq7B9ceMI5w=="
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -18,6 +18,7 @@
|
||||
"css-loader": "^3.5.3",
|
||||
"file-saver": "^2.0.2",
|
||||
"formik": "^2.1.4",
|
||||
"immer": "^8.0.0",
|
||||
"lodash": "^4.17.15",
|
||||
"markdown-to-jsx": "^6.11.4",
|
||||
"moment": "^2.20.1",
|
||||
@ -41,7 +42,8 @@
|
||||
"styled-system": "^5.1.5",
|
||||
"suncalc": "^1.8.0",
|
||||
"urbit-ob": "^5.0.0",
|
||||
"yup": "^0.29.3"
|
||||
"yup": "^0.29.3",
|
||||
"zustand": "^3.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.9.0",
|
||||
|
@ -10,7 +10,7 @@
|
||||
|
||||
<!-- <link rel="manifest" href="%PUBLIC_URL%/manifest.json" /> -->
|
||||
|
||||
<title>OS1</title>
|
||||
<title>Landscape</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
@ -1,6 +1,5 @@
|
||||
import BaseApi from "./base";
|
||||
import { StoreState } from "../store/type";
|
||||
import { BackgroundConfig, LocalUpdateRemoteContentPolicy } from "../types/local-update";
|
||||
|
||||
export default class LocalApi extends BaseApi<StoreState> {
|
||||
getBaseHash() {
|
||||
@ -9,76 +8,6 @@ export default class LocalApi extends BaseApi<StoreState> {
|
||||
});
|
||||
}
|
||||
|
||||
sidebarToggle() {
|
||||
this.store.handleEvent({
|
||||
data: {
|
||||
local: {
|
||||
sidebarToggle: true
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
setDark(isDark: boolean) {
|
||||
this.store.handleEvent({
|
||||
data: {
|
||||
local: {
|
||||
setDark: isDark
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
setOmnibox() {
|
||||
this.store.handleEvent({
|
||||
data: {
|
||||
local: {
|
||||
omniboxShown: true
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
setBackground(backgroundConfig: BackgroundConfig) {
|
||||
this.store.handleEvent({
|
||||
data: {
|
||||
local: {
|
||||
backgroundConfig
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
hideAvatars(hideAvatars: boolean) {
|
||||
this.store.handleEvent({
|
||||
data: {
|
||||
local: {
|
||||
hideAvatars
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
hideNicknames(hideNicknames: boolean) {
|
||||
this.store.handleEvent({
|
||||
data: {
|
||||
local: {
|
||||
hideNicknames
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
setRemoteContentPolicy(policy: LocalUpdateRemoteContentPolicy) {
|
||||
this.store.handleEvent({
|
||||
data: {
|
||||
local: {
|
||||
remoteContentPolicy: policy
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
dehydrate() {
|
||||
this.store.dehydrate();
|
||||
}
|
||||
|
@ -52,13 +52,16 @@ const tokenizeMessage = (text) => {
|
||||
}
|
||||
messages.push({ url: str });
|
||||
message = [];
|
||||
} else if(urbitOb.isValidPatp(str) && !isInCodeBlock) {
|
||||
} else if (urbitOb.isValidPatp(str.replace(/[^a-z\-\~]/g, '')) && !isInCodeBlock) {
|
||||
if (message.length > 0) {
|
||||
// If we're in the middle of a message, add it to the stack and reset
|
||||
messages.push({ text: message.join(' ') });
|
||||
message = [];
|
||||
}
|
||||
messages.push({ mention: str });
|
||||
messages.push({ mention: str.replace(/[^a-z\-\~]/g, '') });
|
||||
if (str.replace(/[a-z\-\~]/g, '').length > 0) {
|
||||
messages.push({ text: str.replace(/[a-z\-\~]/g, '') });
|
||||
}
|
||||
message = [];
|
||||
|
||||
} else {
|
||||
|
@ -50,11 +50,11 @@ const useS3 = (s3: S3State, { accept = '*' } = { accept: '*' }): IuseS3 => {
|
||||
ACL: "public-read",
|
||||
ContentType: file.type,
|
||||
};
|
||||
|
||||
|
||||
setUploading(true);
|
||||
|
||||
const { Location } = await client.current.upload(params).promise();
|
||||
|
||||
|
||||
setUploading(false);
|
||||
|
||||
return Location;
|
||||
@ -75,6 +75,7 @@ const useS3 = (s3: S3State, { accept = '*' } = { accept: '*' }): IuseS3 => {
|
||||
const fileSelector = document.createElement('input');
|
||||
fileSelector.setAttribute('type', 'file');
|
||||
fileSelector.setAttribute('accept', accept);
|
||||
fileSelector.style.visibility = 'hidden';
|
||||
fileSelector.addEventListener('change', () => {
|
||||
const files = fileSelector.files;
|
||||
if (!files || files.length <= 0) {
|
||||
@ -82,10 +83,12 @@ const useS3 = (s3: S3State, { accept = '*' } = { accept: '*' }): IuseS3 => {
|
||||
return;
|
||||
}
|
||||
uploadDefault(files[0]).then(resolve);
|
||||
document.body.removeChild(fileSelector);
|
||||
})
|
||||
document.body.appendChild(fileSelector);
|
||||
fileSelector.click();
|
||||
})
|
||||
|
||||
|
||||
},
|
||||
[uploadDefault]
|
||||
);
|
||||
|
@ -1,7 +1,9 @@
|
||||
import { useEffect } from 'react';
|
||||
import _ from "lodash";
|
||||
import f from "lodash/fp";
|
||||
import f, { memoize } from "lodash/fp";
|
||||
import bigInt, { BigInteger } from "big-integer";
|
||||
import { Contact } from '~/types';
|
||||
import useLocalState from '../state/local';
|
||||
|
||||
export const MOBILE_BROWSER_REGEX = /Android|webOS|iPhone|iPad|iPod|BlackBerry/i;
|
||||
|
||||
@ -353,4 +355,10 @@ export function usePreventWindowUnload(shouldPreventDefault: boolean, message =
|
||||
|
||||
export function pluralize(text: string, isPlural = false, vowel = false) {
|
||||
return isPlural ? `${text}s`: `${vowel ? 'an' : 'a'} ${text}`;
|
||||
}
|
||||
|
||||
// Hide is an optional second parameter for when this function is used in class components
|
||||
export function useShowNickname(contact: Contact | null, hide?: boolean): boolean {
|
||||
const hideNicknames = typeof hide !== 'undefined' ? hide : useLocalState(state => state.hideNicknames);
|
||||
return !!(contact && contact.nickname && !hideNicknames);
|
||||
}
|
@ -8,6 +8,7 @@ import {
|
||||
import { makePatDa } from "~/logic/lib/util";
|
||||
import _ from "lodash";
|
||||
import {StoreState} from "../store/type";
|
||||
import { BigIntOrderedMap } from '../lib/BigIntOrderedMap';
|
||||
|
||||
type HarkState = Pick<StoreState, "notifications" | "notificationsGraphConfig" | "notificationsGroupConfig" | "unreads" | "notificationsChatConfig">;
|
||||
|
||||
@ -128,7 +129,6 @@ function graphWatchSelf(json: any, state: HarkState) {
|
||||
}
|
||||
|
||||
function reduce(data: any, state: HarkState) {
|
||||
console.log(data);
|
||||
unread(data, state);
|
||||
read(data, state);
|
||||
archive(data, state);
|
||||
@ -190,7 +190,7 @@ function unreadEach(json: any, state: HarkState) {
|
||||
function unreads(json: any, state: HarkState) {
|
||||
const data = _.get(json, 'unreads');
|
||||
if(data) {
|
||||
console.log(data);
|
||||
clearState(state);
|
||||
data.forEach(({ index, stats }) => {
|
||||
const { unreads, notifications, last } = stats;
|
||||
updateNotificationStats(state, index, 'notifications', x => x + notifications);
|
||||
@ -206,9 +206,32 @@ function unreads(json: any, state: HarkState) {
|
||||
}
|
||||
}
|
||||
|
||||
function clearState(state){
|
||||
let initialState = {
|
||||
notifications: new BigIntOrderedMap<Timebox>(),
|
||||
archivedNotifications: new BigIntOrderedMap<Timebox>(),
|
||||
notificationsGroupConfig: [],
|
||||
notificationsChatConfig: [],
|
||||
notificationsGraphConfig: {
|
||||
watchOnSelf: false,
|
||||
mentions: false,
|
||||
watching: [],
|
||||
},
|
||||
unreads: {
|
||||
graph: {},
|
||||
group: {}
|
||||
},
|
||||
notificationsCount: 0
|
||||
};
|
||||
|
||||
Object.keys(initialState).forEach(key => {
|
||||
state[key] = initialState[key];
|
||||
});
|
||||
}
|
||||
|
||||
function updateUnreadCount(state: HarkState, index: NotifIndex, count: (c: number) => number) {
|
||||
if(!('graph' in index)) {
|
||||
return;
|
||||
return;
|
||||
}
|
||||
const property = [index.graph.graph, index.graph.index, 'unreads'];
|
||||
const curr = _.get(state.unreads.graph, property, 0);
|
||||
@ -218,7 +241,7 @@ function updateUnreadCount(state: HarkState, index: NotifIndex, count: (c: numbe
|
||||
|
||||
function updateUnreads(state: HarkState, index: NotifIndex, f: (us: Set<string>) => void) {
|
||||
if(!('graph' in index)) {
|
||||
return;
|
||||
return;
|
||||
}
|
||||
const unreads = _.get(state.unreads.graph, [index.graph.graph, index.graph.index, 'unreads'], new Set<string>());
|
||||
const oldSize = unreads.size;
|
||||
@ -238,7 +261,7 @@ function updateNotificationStats(state: HarkState, index: NotifIndex, statField:
|
||||
} else if('group' in index) {
|
||||
const curr = _.get(state.unreads.group, [index.group.group, statField], 0);
|
||||
_.set(state.unreads.group, [index.group.group, statField], f(curr));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function added(json: any, state: HarkState) {
|
||||
|
@ -1,9 +1,9 @@
|
||||
import _ from 'lodash';
|
||||
import { StoreState } from '~/store/type';
|
||||
import { Cage } from '~/types/cage';
|
||||
import { LocalUpdate, BackgroundConfig } from '~/types/local-update';
|
||||
import { LocalUpdate } from '~/types/local-update';
|
||||
|
||||
type LocalState = Pick<StoreState, 'sidebarShown' | 'omniboxShown' | 'baseHash' | 'hideAvatars' | 'hideNicknames' | 'background' | 'dark' | 'suspendedFocus' | 'remoteContentPolicy'>;
|
||||
type LocalState = Pick<StoreState, 'baseHash'>;
|
||||
|
||||
export default class LocalReducer<S extends LocalState> {
|
||||
rehydrate(state: S) {
|
||||
@ -18,20 +18,11 @@ export default class LocalReducer<S extends LocalState> {
|
||||
}
|
||||
|
||||
dehydrate(state: S) {
|
||||
const json = _.pick(state, ['hideNicknames' , 'hideAvatars' , 'background', 'remoteContentPolicy']);
|
||||
localStorage.setItem('localReducer', JSON.stringify(json));
|
||||
}
|
||||
reduce(json: Cage, state: S) {
|
||||
const data = json['local'];
|
||||
if (data) {
|
||||
this.sidebarToggle(data, state);
|
||||
this.setDark(data, state);
|
||||
this.baseHash(data, state);
|
||||
this.backgroundConfig(data, state)
|
||||
this.hideAvatars(data, state)
|
||||
this.hideNicknames(data, state)
|
||||
this.omniboxShown(data, state);
|
||||
this.remoteContentPolicy(data, state);
|
||||
}
|
||||
}
|
||||
baseHash(obj: LocalUpdate, state: S) {
|
||||
@ -39,53 +30,4 @@ export default class LocalReducer<S extends LocalState> {
|
||||
state.baseHash = obj.baseHash;
|
||||
}
|
||||
}
|
||||
|
||||
omniboxShown(obj: LocalUpdate, state: S) {
|
||||
if ('omniboxShown' in obj) {
|
||||
state.omniboxShown = !state.omniboxShown;
|
||||
if (state.suspendedFocus) {
|
||||
state.suspendedFocus.focus();
|
||||
state.suspendedFocus = null;
|
||||
} else {
|
||||
state.suspendedFocus = document.activeElement;
|
||||
document.activeElement?.blur();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sidebarToggle(obj: LocalUpdate, state: S) {
|
||||
if ('sidebarToggle' in obj) {
|
||||
state.sidebarShown = !state.sidebarShown;
|
||||
}
|
||||
}
|
||||
|
||||
setDark(obj: LocalUpdate, state: S) {
|
||||
if('setDark' in obj) {
|
||||
state.dark = obj.setDark;
|
||||
}
|
||||
}
|
||||
|
||||
backgroundConfig(obj: LocalUpdate, state: S) {
|
||||
if('backgroundConfig' in obj) {
|
||||
state.background = obj.backgroundConfig;
|
||||
}
|
||||
}
|
||||
|
||||
remoteContentPolicy(obj: LocalUpdate, state: S) {
|
||||
if('remoteContentPolicy' in obj) {
|
||||
state.remoteContentPolicy = obj.remoteContentPolicy;
|
||||
}
|
||||
}
|
||||
|
||||
hideAvatars(obj: LocalUpdate, state: S) {
|
||||
if('hideAvatars' in obj) {
|
||||
state.hideAvatars = obj.hideAvatars;
|
||||
}
|
||||
}
|
||||
|
||||
hideNicknames(obj: LocalUpdate, state: S) {
|
||||
if( 'hideNicknames' in obj) {
|
||||
state.hideNicknames = obj.hideNicknames;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
58
pkg/interface/src/logic/state/local.tsx
Normal file
58
pkg/interface/src/logic/state/local.tsx
Normal file
@ -0,0 +1,58 @@
|
||||
import React, { ReactNode } from "react";
|
||||
import create, { State } from 'zustand';
|
||||
import { persist } from 'zustand/middleware';
|
||||
import produce from 'immer';
|
||||
import { BackgroundConfig, RemoteContentPolicy } from "~/types/local-update";
|
||||
|
||||
export interface LocalState extends State {
|
||||
hideAvatars: boolean;
|
||||
hideNicknames: boolean;
|
||||
remoteContentPolicy: RemoteContentPolicy;
|
||||
dark: boolean;
|
||||
background: BackgroundConfig;
|
||||
omniboxShown: boolean;
|
||||
suspendedFocus?: HTMLElement;
|
||||
toggleOmnibox: () => void;
|
||||
set: (fn: (state: LocalState) => void) => void
|
||||
};
|
||||
|
||||
const useLocalState = create<LocalState>(persist((set, get) => ({
|
||||
dark: false,
|
||||
background: undefined,
|
||||
hideAvatars: false,
|
||||
hideNicknames: false,
|
||||
remoteContentPolicy: {
|
||||
imageShown: true,
|
||||
audioShown: true,
|
||||
videoShown: true,
|
||||
oembedShown: true,
|
||||
},
|
||||
omniboxShown: false,
|
||||
suspendedFocus: undefined,
|
||||
toggleOmnibox: () => set(produce(state => {
|
||||
state.omniboxShown = !state.omniboxShown;
|
||||
if (state.suspendedFocus) {
|
||||
state.suspendedFocus.focus();
|
||||
state.suspendedFocus = undefined;
|
||||
} else {
|
||||
state.suspendedFocus = document.activeElement;
|
||||
state.suspendedFocus.blur();
|
||||
}
|
||||
})),
|
||||
set: fn => set(produce(fn))
|
||||
}), {
|
||||
name: 'localReducer'
|
||||
}));
|
||||
|
||||
function withLocalState<P, S extends keyof LocalState>(Component: any, stateMemberKeys?: S[]) {
|
||||
return React.forwardRef((props: Omit<P, S>, ref) => {
|
||||
const localState = stateMemberKeys ? useLocalState(
|
||||
state => stateMemberKeys.reduce(
|
||||
(object, key) => ({ ...object, [key]: state[key] }), {}
|
||||
)
|
||||
): useLocalState();
|
||||
return <Component ref={ref} {...localState} {...props} />
|
||||
});
|
||||
}
|
||||
|
||||
export { useLocalState as default, withLocalState };
|
@ -51,19 +51,7 @@ export default class GlobalStore extends BaseStore<StoreState> {
|
||||
initialState(): StoreState {
|
||||
return {
|
||||
connection: 'connected',
|
||||
sidebarShown: true,
|
||||
omniboxShown: false,
|
||||
suspendedFocus: null,
|
||||
baseHash: null,
|
||||
background: undefined,
|
||||
remoteContentPolicy: {
|
||||
imageShown: true,
|
||||
audioShown: true,
|
||||
videoShown: true,
|
||||
oembedShown: true,
|
||||
},
|
||||
hideAvatars: false,
|
||||
hideNicknames: false,
|
||||
invites: {},
|
||||
associations: {
|
||||
contacts: {},
|
||||
@ -88,7 +76,6 @@ export default class GlobalStore extends BaseStore<StoreState> {
|
||||
credentials: null
|
||||
},
|
||||
contacts: {},
|
||||
dark: false,
|
||||
notifications: new BigIntOrderedMap<Timebox>(),
|
||||
archivedNotifications: new BigIntOrderedMap<Timebox>(),
|
||||
notificationsGroupConfig: [],
|
||||
|
@ -11,23 +11,13 @@ import {
|
||||
Notifications,
|
||||
NotificationGraphConfig,
|
||||
GroupNotificationsConfig,
|
||||
LocalUpdateRemoteContentPolicy,
|
||||
BackgroundConfig,
|
||||
Unreads
|
||||
} from "~/types";
|
||||
|
||||
export interface StoreState {
|
||||
// local state
|
||||
sidebarShown: boolean;
|
||||
omniboxShown: boolean;
|
||||
suspendedFocus: HTMLInputElement | null;
|
||||
dark: boolean;
|
||||
connection: ConnectionStatus;
|
||||
baseHash: string | null;
|
||||
background: BackgroundConfig;
|
||||
remoteContentPolicy: LocalUpdateRemoteContentPolicy;
|
||||
hideAvatars: boolean;
|
||||
hideNicknames: boolean;
|
||||
|
||||
// invite state
|
||||
invites: Invites;
|
||||
|
@ -1,7 +1,3 @@
|
||||
interface LocalUpdateSidebarToggle {
|
||||
sidebarToggle: boolean;
|
||||
}
|
||||
|
||||
interface LocalUpdateSetDark {
|
||||
setDark: boolean;
|
||||
}
|
||||
@ -26,7 +22,7 @@ interface LocalUpdateSetOmniboxShown {
|
||||
omniboxShown: boolean;
|
||||
}
|
||||
|
||||
export interface LocalUpdateRemoteContentPolicy {
|
||||
export interface RemoteContentPolicy {
|
||||
imageShown: boolean;
|
||||
audioShown: boolean;
|
||||
videoShown: boolean;
|
||||
@ -46,11 +42,10 @@ interface BackgroundConfigColor {
|
||||
export type BackgroundConfig = BackgroundConfigUrl | BackgroundConfigColor | undefined;
|
||||
|
||||
export type LocalUpdate =
|
||||
LocalUpdateSidebarToggle
|
||||
| LocalUpdateSetDark
|
||||
| LocalUpdateBaseHash
|
||||
| LocalUpdateBackgroundConfig
|
||||
| LocalUpdateHideAvatars
|
||||
| LocalUpdateHideNicknames
|
||||
| LocalUpdateSetOmniboxShown
|
||||
| LocalUpdateRemoteContentPolicy;
|
||||
| RemoteContentPolicy;
|
@ -26,6 +26,7 @@ import GlobalSubscription from '~/logic/subscription/global';
|
||||
import GlobalApi from '~/logic/api/global';
|
||||
import { uxToHex } from '~/logic/lib/util';
|
||||
import { foregroundFromBackground } from '~/logic/lib/sigil';
|
||||
import { withLocalState } from '~/logic/state/local';
|
||||
|
||||
|
||||
const Root = styled.div`
|
||||
@ -86,23 +87,29 @@ class App extends React.Component {
|
||||
componentDidMount() {
|
||||
this.subscription.start();
|
||||
this.themeWatcher = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
this.api.local.setDark(this.themeWatcher.matches);
|
||||
this.themeWatcher.addListener(this.updateTheme);
|
||||
this.themeWatcher.onchange = this.updateTheme;
|
||||
setTimeout(() => {
|
||||
// Something about how the store works doesn't like changing it
|
||||
// before the app has actually rendered, hence the timeout
|
||||
this.updateTheme(this.themeWatcher);
|
||||
}, 500);
|
||||
this.api.local.getBaseHash();
|
||||
this.store.rehydrate();
|
||||
Mousetrap.bindGlobal(['command+/', 'ctrl+/'], (e) => {
|
||||
e.preventDefault();
|
||||
e.stopImmediatePropagation();
|
||||
this.api.local.setOmnibox();
|
||||
this.props.toggleOmnibox();
|
||||
});
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.themeWatcher.removeListener(this.updateTheme);
|
||||
this.themeWatcher.onchange = undefined;
|
||||
}
|
||||
|
||||
updateTheme(e) {
|
||||
this.api.local.setDark(e.matches);
|
||||
this.props.set(state => {
|
||||
state.dark = e.matches;
|
||||
});
|
||||
}
|
||||
|
||||
faviconString() {
|
||||
@ -122,11 +129,11 @@ class App extends React.Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { state } = this;
|
||||
const { state, props } = this;
|
||||
const associations = state.associations ?
|
||||
state.associations : { contacts: {} };
|
||||
const theme = state.dark ? dark : light;
|
||||
const { background } = state;
|
||||
const theme = props.dark ? dark : light;
|
||||
const background = this.props.background;
|
||||
|
||||
const notificationsCount = state.notificationsCount || 0;
|
||||
const doNotDisturb = state.doNotDisturb || false;
|
||||
@ -164,7 +171,7 @@ class App extends React.Component {
|
||||
notifications={state.notificationsCount}
|
||||
invites={state.invites}
|
||||
groups={state.groups}
|
||||
show={state.omniboxShown}
|
||||
show={this.props.omniboxShown}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
<ErrorBoundary>
|
||||
@ -183,5 +190,5 @@ class App extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
export default process.env.NODE_ENV === 'production' ? App : hot(App);
|
||||
export default withLocalState(process.env.NODE_ENV === 'production' ? App : hot(App));
|
||||
|
||||
|
@ -87,7 +87,6 @@ export function ChatResource(props: ChatResourceProps) {
|
||||
<Col {...bind} height="100%" overflow="hidden" position="relative">
|
||||
{dragging && <SubmitDragger />}
|
||||
<ChatWindow
|
||||
remoteContentPolicy={props.remoteContentPolicy}
|
||||
mailboxSize={5}
|
||||
match={props.match as any}
|
||||
stationPendingMessages={[]}
|
||||
@ -105,8 +104,6 @@ export function ChatResource(props: ChatResourceProps) {
|
||||
ship={owner}
|
||||
station={station}
|
||||
api={props.api}
|
||||
hideNicknames={props.hideNicknames}
|
||||
hideAvatars={props.hideAvatars}
|
||||
location={props.location}
|
||||
scrollTo={scrollTo ? parseInt(scrollTo, 10) : undefined}
|
||||
/>
|
||||
@ -119,7 +116,6 @@ export function ChatResource(props: ChatResourceProps) {
|
||||
contacts={contacts}
|
||||
onUnmount={appendUnsent}
|
||||
s3={props.s3}
|
||||
hideAvatars={props.hideAvatars}
|
||||
placeholder="Message..."
|
||||
message={unsent[station] || ''}
|
||||
deleteMessage={clearUnsent}
|
||||
|
@ -10,6 +10,7 @@ import { Envelope } from '~/types/chat-update';
|
||||
import { Contacts, Content } from '~/types';
|
||||
import { Row, BaseImage, Box, Icon, LoadingSpinner } from '@tlon/indigo-react';
|
||||
import withS3 from '~/views/components/withS3';
|
||||
import { withLocalState } from '~/logic/state/local';
|
||||
|
||||
type ChatInputProps = IuseS3 & {
|
||||
api: GlobalApi;
|
||||
@ -66,7 +67,7 @@ class ChatInput extends Component<ChatInputProps, ChatInputState> {
|
||||
inCodeMode: false
|
||||
}, async () => {
|
||||
const output = await props.api.graph.eval(text);
|
||||
const contents: Content[] = [{ code: { output, expression: text }}];
|
||||
const contents: Content[] = [{ code: { output, expression: text }}];
|
||||
const post = createPost(contents);
|
||||
props.api.graph.addPost(ship, name, post);
|
||||
});
|
||||
@ -199,4 +200,4 @@ class ChatInput extends Component<ChatInputProps, ChatInputState> {
|
||||
}
|
||||
}
|
||||
|
||||
export default withS3(ChatInput, {accept: 'image/*'});
|
||||
export default withLocalState(withS3(ChatInput, {accept: 'image/*'}), ['hideAvatars']);
|
||||
|
@ -4,14 +4,14 @@ import _ from "lodash";
|
||||
import { Box, Row, Text, Rule } from "@tlon/indigo-react";
|
||||
|
||||
import OverlaySigil from '~/views/components/OverlaySigil';
|
||||
import { uxToHex, cite, writeText } from '~/logic/lib/util';
|
||||
import { Envelope, IMessage } from "~/types/chat-update";
|
||||
import { Group, Association, Contacts, LocalUpdateRemoteContentPolicy, Post } from "~/types";
|
||||
import { uxToHex, cite, writeText, useShowNickname } from '~/logic/lib/util';
|
||||
import { Group, Association, Contacts, Post } from "~/types";
|
||||
import TextContent from './content/text';
|
||||
import CodeContent from './content/code';
|
||||
import RemoteContent from '~/views/components/RemoteContent';
|
||||
import { Mention } from "~/views/components/MentionText";
|
||||
import styled from "styled-components";
|
||||
import useLocalState from "~/logic/state/local";
|
||||
|
||||
export const DATESTAMP_FORMAT = '[~]YYYY.M.D';
|
||||
|
||||
@ -39,9 +39,6 @@ interface ChatMessageProps {
|
||||
group: Group;
|
||||
association: Association;
|
||||
contacts: Contacts;
|
||||
hideAvatars: boolean;
|
||||
hideNicknames: boolean;
|
||||
remoteContentPolicy: LocalUpdateRemoteContentPolicy;
|
||||
className?: string;
|
||||
isPending: boolean;
|
||||
style?: any;
|
||||
@ -76,9 +73,6 @@ export default class ChatMessage extends Component<ChatMessageProps> {
|
||||
group,
|
||||
association,
|
||||
contacts,
|
||||
hideAvatars,
|
||||
hideNicknames,
|
||||
remoteContentPolicy,
|
||||
className = '',
|
||||
isPending,
|
||||
style,
|
||||
@ -109,11 +103,8 @@ export default class ChatMessage extends Component<ChatMessageProps> {
|
||||
msg,
|
||||
timestamp,
|
||||
contacts,
|
||||
hideNicknames,
|
||||
association,
|
||||
group,
|
||||
hideAvatars,
|
||||
remoteContentPolicy,
|
||||
measure: reboundMeasure.bind(this),
|
||||
style,
|
||||
containerClass,
|
||||
@ -162,9 +153,6 @@ interface MessageProps {
|
||||
group: Group;
|
||||
association: Association;
|
||||
contacts: Contacts;
|
||||
hideAvatars: boolean;
|
||||
hideNicknames: boolean;
|
||||
remoteContentPolicy: LocalUpdateRemoteContentPolicy;
|
||||
containerClass: string;
|
||||
isPending: boolean;
|
||||
style: any;
|
||||
@ -172,105 +160,96 @@ interface MessageProps {
|
||||
scrollWindow: HTMLDivElement;
|
||||
};
|
||||
|
||||
export class MessageWithSigil extends PureComponent<MessageProps> {
|
||||
isDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
export const MessageWithSigil = (props) => {
|
||||
const {
|
||||
msg,
|
||||
timestamp,
|
||||
contacts,
|
||||
association,
|
||||
group,
|
||||
measure,
|
||||
api,
|
||||
history,
|
||||
scrollWindow,
|
||||
fontSize
|
||||
} = props;
|
||||
|
||||
render() {
|
||||
const {
|
||||
msg,
|
||||
timestamp,
|
||||
contacts,
|
||||
hideNicknames,
|
||||
association,
|
||||
group,
|
||||
hideAvatars,
|
||||
remoteContentPolicy,
|
||||
measure,
|
||||
api,
|
||||
history,
|
||||
scrollWindow,
|
||||
fontSize
|
||||
} = this.props;
|
||||
const dark = useLocalState(state => state.dark);
|
||||
|
||||
const datestamp = moment.unix(msg['time-sent'] / 1000).format(DATESTAMP_FORMAT);
|
||||
const contact = msg.author in contacts ? contacts[msg.author] : false;
|
||||
const showNickname = !hideNicknames && contact && contact.nickname;
|
||||
const name = showNickname ? contact!.nickname : cite(msg.author);
|
||||
const color = contact ? `#${uxToHex(contact.color)}` : this.isDark ? '#000000' :'#FFFFFF'
|
||||
const sigilClass = contact ? '' : this.isDark ? 'mix-blend-diff' : 'mix-blend-darken';
|
||||
const datestamp = moment.unix(msg['time-sent'] / 1000).format(DATESTAMP_FORMAT);
|
||||
const contact = msg.author in contacts ? contacts[msg.author] : false;
|
||||
const showNickname = useShowNickname(contact);
|
||||
const name = showNickname ? contact.nickname : cite(msg.author);
|
||||
const color = contact ? `#${uxToHex(contact.color)}` : dark ? '#000000' :'#FFFFFF'
|
||||
const sigilClass = contact ? '' : dark ? 'mix-blend-diff' : 'mix-blend-darken';
|
||||
|
||||
let nameSpan = null;
|
||||
let nameSpan = null;
|
||||
|
||||
const copyNotice = (saveName) => {
|
||||
if (nameSpan !== null) {
|
||||
nameSpan.innerText = 'Copied';
|
||||
setTimeout(() => {
|
||||
nameSpan.innerText = saveName;
|
||||
}, 800);
|
||||
}
|
||||
};
|
||||
const copyNotice = (saveName) => {
|
||||
if (nameSpan !== null) {
|
||||
nameSpan.innerText = 'Copied';
|
||||
setTimeout(() => {
|
||||
nameSpan.innerText = saveName;
|
||||
}, 800);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<OverlaySigil
|
||||
ship={msg.author}
|
||||
contact={contact}
|
||||
color={color}
|
||||
sigilClass={sigilClass}
|
||||
group={group}
|
||||
hideAvatars={hideAvatars}
|
||||
hideNicknames={hideNicknames}
|
||||
scrollWindow={scrollWindow}
|
||||
history={history}
|
||||
api={api}
|
||||
bg="white"
|
||||
className="fl pr3 v-top pt1"
|
||||
/>
|
||||
<Box flexGrow={1} display='block' className="clamp-message">
|
||||
<Box
|
||||
return (
|
||||
<>
|
||||
<OverlaySigil
|
||||
ship={msg.author}
|
||||
contact={contact}
|
||||
color={color}
|
||||
sigilClass={sigilClass}
|
||||
group={group}
|
||||
scrollWindow={scrollWindow}
|
||||
history={history}
|
||||
api={api}
|
||||
bg="white"
|
||||
className="fl pr3 v-top pt1"
|
||||
/>
|
||||
<Box flexGrow={1} display='block' className="clamp-message">
|
||||
<Box
|
||||
flexShrink={0}
|
||||
className="hide-child"
|
||||
pt={1}
|
||||
pb={1}
|
||||
display='flex'
|
||||
alignItems='center'
|
||||
>
|
||||
<Text
|
||||
fontSize={0}
|
||||
mr={3}
|
||||
flexShrink={0}
|
||||
className="hide-child"
|
||||
pt={1}
|
||||
pb={1}
|
||||
display='flex'
|
||||
alignItems='center'
|
||||
>
|
||||
<Text
|
||||
fontSize={0}
|
||||
mr={3}
|
||||
flexShrink={0}
|
||||
mono={!showNickname}
|
||||
fontWeight={(showNickname) ? '500' : '400'}
|
||||
className={`mw5 db truncate pointer`}
|
||||
ref={e => nameSpan = e}
|
||||
onClick={() => {
|
||||
writeText(`~${msg.author}`);
|
||||
copyNotice(name);
|
||||
}}
|
||||
title={`~${msg.author}`}
|
||||
>{name}</Text>
|
||||
<Text flexShrink='0' gray mono className="v-mid">{timestamp}</Text>
|
||||
<Text flexShrink={0} gray mono ml={2} className="v-mid child dn-s">{datestamp}</Text>
|
||||
</Box>
|
||||
<ContentBox flexShrink={0} fontSize={fontSize ? fontSize : '14px'}>
|
||||
{msg.contents.map(c =>
|
||||
<MessageContent
|
||||
contacts={contacts}
|
||||
content={c}
|
||||
remoteContentPolicy={remoteContentPolicy}
|
||||
measure={measure}
|
||||
fontSize={fontSize}
|
||||
group={group}
|
||||
hideNicknames={hideNicknames}
|
||||
hideAvatars={hideAvatars}
|
||||
/>)}
|
||||
</ContentBox>
|
||||
mono={!showNickname}
|
||||
fontWeight={(showNickname) ? '500' : '400'}
|
||||
className={`mw5 db truncate pointer`}
|
||||
ref={e => nameSpan = e}
|
||||
onClick={() => {
|
||||
writeText(`~${msg.author}`);
|
||||
copyNotice(name);
|
||||
}}
|
||||
title={`~${msg.author}`}
|
||||
>{name}</Text>
|
||||
<Text flexShrink='0' gray mono className="v-mid">{timestamp}</Text>
|
||||
<Text flexShrink={0} gray mono ml={2} className="v-mid child dn-s">{datestamp}</Text>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
}
|
||||
<ContentBox flexShrink={0} fontSize={fontSize ? fontSize : '14px'}>
|
||||
{msg.contents.map(c =>
|
||||
<MessageContent
|
||||
contacts={contacts}
|
||||
content={c}
|
||||
measure={measure}
|
||||
fontSize={fontSize}
|
||||
group={group}
|
||||
/>)}
|
||||
</ContentBox>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
const ContentBox = styled(Box)`
|
||||
& > :first-child {
|
||||
margin-left: 0px;
|
||||
@ -278,7 +257,7 @@ const ContentBox = styled(Box)`
|
||||
|
||||
`;
|
||||
|
||||
export const MessageWithoutSigil = ({ timestamp, contacts, msg, remoteContentPolicy, measure, group, hideNicknames, hideAvatars }) => (
|
||||
export const MessageWithoutSigil = ({ timestamp, contacts, msg, measure, group }) => (
|
||||
<>
|
||||
<Text flexShrink={0} mono gray display='inline-block' pt='2px' lineHeight='tall' className="child">{timestamp}</Text>
|
||||
<ContentBox flexShrink={0} fontSize='14px' className="clamp-message" style={{ flexGrow: 1 }}>
|
||||
@ -288,15 +267,12 @@ export const MessageWithoutSigil = ({ timestamp, contacts, msg, remoteContentPol
|
||||
contacts={contacts}
|
||||
content={c}
|
||||
group={group}
|
||||
remoteContentPolicy={remoteContentPolicy}
|
||||
measure={measure}
|
||||
hideNicknames={hideNicknames}
|
||||
hideAvatars={hideAvatars}/>))}
|
||||
measure={measure}/>))}
|
||||
</ContentBox>
|
||||
</>
|
||||
);
|
||||
|
||||
export const MessageContent = ({ content, contacts, remoteContentPolicy, measure, fontSize, group, hideNicknames, hideAvatars }) => {
|
||||
export const MessageContent = ({ content, contacts, measure, fontSize, group }) => {
|
||||
if ('code' in content) {
|
||||
return <CodeContent content={content} />;
|
||||
} else if ('url' in content) {
|
||||
@ -304,7 +280,6 @@ export const MessageContent = ({ content, contacts, remoteContentPolicy, measure
|
||||
<Box mx="2px" flexShrink={0} fontSize={fontSize ? fontSize : '14px'} lineHeight="tall" color='black'>
|
||||
<RemoteContent
|
||||
url={content.url}
|
||||
remoteContentPolicy={remoteContentPolicy}
|
||||
onLoad={measure}
|
||||
imageProps={{style: {
|
||||
maxWidth: '18rem',
|
||||
@ -325,7 +300,7 @@ export const MessageContent = ({ content, contacts, remoteContentPolicy, measure
|
||||
} else if ('text' in content) {
|
||||
return <TextContent fontSize={fontSize} content={content} />;
|
||||
} else if ('mention' in content) {
|
||||
return <Mention group={group} ship={content.mention} contact={contacts?.[content.mention]} hideNicknames={hideNicknames} hideAvatars={hideAvatars} />
|
||||
return <Mention group={group} ship={content.mention} contact={contacts?.[content.mention]} />
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
@ -9,8 +9,7 @@ import { Contacts } from "~/types/contact-update";
|
||||
import { Association } from "~/types/metadata-update";
|
||||
import { Group } from "~/types/group-update";
|
||||
import { Envelope, IMessage } from "~/types/chat-update";
|
||||
import { LocalUpdateRemoteContentPolicy, Graph } from "~/types";
|
||||
import { BigIntOrderedMap } from "~/logic/lib/BigIntOrderedMap";
|
||||
import { Graph } from "~/types";
|
||||
|
||||
import VirtualScroller from "~/views/components/VirtualScroller";
|
||||
|
||||
@ -41,9 +40,6 @@ type ChatWindowProps = RouteComponentProps<{
|
||||
ship: Patp;
|
||||
station: any;
|
||||
api: GlobalApi;
|
||||
hideNicknames: boolean;
|
||||
hideAvatars: boolean;
|
||||
remoteContentPolicy: LocalUpdateRemoteContentPolicy;
|
||||
scrollTo?: number;
|
||||
}
|
||||
|
||||
@ -253,19 +249,16 @@ export default class ChatWindow extends Component<ChatWindowProps, ChatWindowSta
|
||||
contacts,
|
||||
mailboxSize,
|
||||
graph,
|
||||
hideAvatars,
|
||||
hideNicknames,
|
||||
remoteContentPolicy,
|
||||
history
|
||||
} = this.props;
|
||||
|
||||
const unreadMarkerRef = this.unreadMarkerRef;
|
||||
|
||||
|
||||
const messageProps = { association, group, contacts, hideAvatars, hideNicknames, remoteContentPolicy, unreadMarkerRef, history, api };
|
||||
const messageProps = { association, group, contacts, unreadMarkerRef, history, api };
|
||||
|
||||
const keys = graph.keys().reverse();
|
||||
const unreadIndex = keys[this.props.unreadCount];
|
||||
const unreadIndex = graph.keys()[this.props.unreadCount];
|
||||
const unreadMsg = unreadIndex && graph.get(unreadIndex);
|
||||
|
||||
return (
|
||||
|
@ -2,8 +2,9 @@ import React, { Component } from 'react';
|
||||
import { UnControlled as CodeEditor } from 'react-codemirror2';
|
||||
import { MOBILE_BROWSER_REGEX } from "~/logic/lib/util";
|
||||
import CodeMirror from 'codemirror';
|
||||
import styled from "styled-components";
|
||||
|
||||
import { Row, BaseTextArea } from '@tlon/indigo-react';
|
||||
import { Row, BaseTextArea, Box } from '@tlon/indigo-react';
|
||||
|
||||
import 'codemirror/mode/markdown/markdown';
|
||||
import 'codemirror/addon/display/placeholder';
|
||||
@ -52,9 +53,40 @@ const inputProxy = (input) => new Proxy(input, {
|
||||
if (property === 'setValue') {
|
||||
return (val) => target.value = val;
|
||||
}
|
||||
if (property === 'element') {
|
||||
return input;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const MobileBox = styled(Box)`
|
||||
display: inline-grid;
|
||||
vertical-align: center;
|
||||
align-items: stretch;
|
||||
position: relative;
|
||||
justify-content: flex-start;
|
||||
width: 100%;
|
||||
|
||||
&:after,
|
||||
textarea {
|
||||
grid-area: 2 / 1;
|
||||
width: auto;
|
||||
min-width: 1em;
|
||||
font: inherit;
|
||||
padding: 0.25em;
|
||||
margin: 0;
|
||||
resize: none;
|
||||
background: none;
|
||||
appearance: none;
|
||||
border: none;
|
||||
}
|
||||
&::after {
|
||||
content: attr(data-value) ' ';
|
||||
visibility: hidden;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
`;
|
||||
|
||||
export default class ChatEditor extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
@ -167,25 +199,41 @@ export default class ChatEditor extends Component {
|
||||
color="black"
|
||||
>
|
||||
{MOBILE_BROWSER_REGEX.test(navigator.userAgent)
|
||||
? <BaseTextArea
|
||||
fontFamily={inCodeMode ? 'Source Code Pro' : 'Inter'}
|
||||
fontSize="14px"
|
||||
lineHeight="tall"
|
||||
style={{ width: '100%', background: 'transparent', color: 'currentColor' }}
|
||||
placeholder={inCodeMode ? "Code..." : "Message..."}
|
||||
onKeyUp={event => {
|
||||
if (event.key === 'Enter') {
|
||||
this.submit();
|
||||
} else {
|
||||
? <MobileBox
|
||||
data-value={this.state.message}
|
||||
fontSize="14px"
|
||||
lineHeight="tall"
|
||||
onClick={event => {
|
||||
if (this.editor) {
|
||||
this.editor.element.focus();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<BaseTextArea
|
||||
fontFamily={inCodeMode ? 'Source Code Pro' : 'Inter'}
|
||||
fontSize="14px"
|
||||
lineHeight="tall"
|
||||
rows="1"
|
||||
style={{ width: '100%', background: 'transparent', color: 'currentColor' }}
|
||||
placeholder={inCodeMode ? "Code..." : "Message..."}
|
||||
onChange={event => {
|
||||
this.messageChange(null, null, event.target.value);
|
||||
}
|
||||
}}
|
||||
ref={input => {
|
||||
if (!input) return;
|
||||
this.editor = inputProxy(input);
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
}}
|
||||
onKeyDown={event => {
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
this.submit();
|
||||
} else {
|
||||
this.messageChange(null, null, event.target.value);
|
||||
}
|
||||
}}
|
||||
ref={input => {
|
||||
if (!input) return;
|
||||
this.editor = inputProxy(input);
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
</MobileBox>
|
||||
: <CodeEditor
|
||||
className="lh-copy"
|
||||
value={message}
|
||||
|
@ -14,10 +14,7 @@ export default class GraphApp extends PureComponent {
|
||||
const graphKeys = props.graphKeys || new Set([]);
|
||||
const graphs = props.graphs || {};
|
||||
|
||||
const {
|
||||
api, sidebarShown, s3,
|
||||
hideAvatars, hideNicknames, remoteContentPolicy
|
||||
} = this.props;
|
||||
const { api } = this.props;
|
||||
|
||||
return (
|
||||
<Switch>
|
||||
|
@ -1,5 +1,4 @@
|
||||
import React, { useState } from 'react';
|
||||
import Helmet from 'react-helmet';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { Box, Row, Icon, Text } from '@tlon/indigo-react';
|
||||
@ -14,6 +13,7 @@ import ModalButton from './components/ModalButton';
|
||||
import { writeText } from '~/logic/lib/util';
|
||||
import { NewGroup } from "~/views/landscape/components/NewGroup";
|
||||
import { JoinGroup } from "~/views/landscape/components/JoinGroup";
|
||||
import { Helmet } from 'react-helmet';
|
||||
|
||||
const ScrollbarLessBox = styled(Box)`
|
||||
scrollbar-width: none !important;
|
||||
@ -25,13 +25,38 @@ const ScrollbarLessBox = styled(Box)`
|
||||
|
||||
export default function LaunchApp(props) {
|
||||
const [hashText, setHashText] = useState(props.baseHash);
|
||||
|
||||
const hashBox = (
|
||||
<Box
|
||||
position={["relative", "absolute"]}
|
||||
fontFamily="mono"
|
||||
left="0"
|
||||
bottom="0"
|
||||
color="scales.black20"
|
||||
bg="white"
|
||||
ml={3}
|
||||
mb={3}
|
||||
borderRadius={2}
|
||||
fontSize={0}
|
||||
p={2}
|
||||
boxShadow="0 0 0px 1px inset"
|
||||
cursor="pointer"
|
||||
onClick={() => {
|
||||
writeText(props.baseHash);
|
||||
setHashText('copied');
|
||||
setTimeout(() => {
|
||||
setHashText(props.baseHash);
|
||||
}, 2000);
|
||||
}}
|
||||
>
|
||||
<Text color="gray">{hashText || props.baseHash}</Text>
|
||||
</Box>
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>OS1 - Home</title>
|
||||
<Helmet defer={false}>
|
||||
<title>{ props.notificationsCount ? `(${String(props.notificationsCount) }) `: '' }Landscape</title>
|
||||
</Helmet>
|
||||
<ScrollbarLessBox height='100%' overflowY='scroll'>
|
||||
<ScrollbarLessBox height='100%' overflowY='scroll' display="flex" flexDirection="column">
|
||||
<Welcome firstTime={props.launch.firstTime} api={props.api} />
|
||||
<Box
|
||||
mx='2'
|
||||
@ -83,30 +108,9 @@ export default function LaunchApp(props) {
|
||||
</ModalButton>
|
||||
<Groups unreads={props.unreads} groups={props.groups} associations={props.associations} />
|
||||
</Box>
|
||||
<Box alignSelf="flex-start" display={["block", "none"]}>{hashBox}</Box>
|
||||
</ScrollbarLessBox>
|
||||
<Box
|
||||
position="absolute"
|
||||
fontFamily="mono"
|
||||
left="0"
|
||||
bottom="0"
|
||||
color="gray"
|
||||
bg="white"
|
||||
ml={3}
|
||||
mb={3}
|
||||
borderRadius={2}
|
||||
fontSize={0}
|
||||
p={2}
|
||||
cursor="pointer"
|
||||
onClick={() => {
|
||||
writeText(props.baseHash);
|
||||
setHashText('copied');
|
||||
setTimeout(() => {
|
||||
setHashText(props.baseHash);
|
||||
}, 2000);
|
||||
}}
|
||||
>
|
||||
{hashText || props.baseHash}
|
||||
</Box>
|
||||
<Box display={["none", "block"]}>{hashBox}</Box>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ import ErrorBoundary from '~/views/components/ErrorBoundary';
|
||||
import Tile from './tile';
|
||||
|
||||
export const weatherStyleMap = {
|
||||
Clear: 'rgba(67, 169, 255, 0.4)',
|
||||
Sunny: 'rgba(67, 169, 255, 0.4)',
|
||||
PartlyCloudy: 'rgba(178, 211, 255, 0.33)',
|
||||
Cloudy: 'rgba(136, 153, 176, 0.43)',
|
||||
|
@ -10,7 +10,6 @@ import { RouteComponentProps } from "react-router-dom";
|
||||
|
||||
import { LinkItem } from "./components/LinkItem";
|
||||
import LinkSubmit from "./components/LinkSubmit";
|
||||
import { LinkPreview } from "./components/link-preview";
|
||||
import { Comments } from "~/views/components/Comments";
|
||||
|
||||
import "./css/custom.css";
|
||||
@ -33,9 +32,6 @@ export function LinkResource(props: LinkResourceProps) {
|
||||
graphKeys,
|
||||
unreads,
|
||||
s3,
|
||||
hideAvatars,
|
||||
hideNicknames,
|
||||
remoteContentPolicy,
|
||||
history
|
||||
} = props;
|
||||
|
||||
@ -85,9 +81,6 @@ export function LinkResource(props: LinkResourceProps) {
|
||||
contacts={contactDetails}
|
||||
unreads={unreads}
|
||||
nickname={contact?.nickname}
|
||||
hideAvatars={hideAvatars}
|
||||
hideNicknames={hideNicknames}
|
||||
remoteContentPolicy={remoteContentPolicy}
|
||||
baseUrl={resourceUrl}
|
||||
group={group}
|
||||
path={resource["group-path"]}
|
||||
@ -126,9 +119,6 @@ export function LinkResource(props: LinkResourceProps) {
|
||||
key={node.post.index}
|
||||
resource={resourcePath}
|
||||
node={node}
|
||||
hideAvatars={hideAvatars}
|
||||
hideNicknames={hideNicknames}
|
||||
remoteContentPolicy={remoteContentPolicy}
|
||||
baseUrl={resourceUrl}
|
||||
unreads={unreads}
|
||||
group={group}
|
||||
@ -145,9 +135,6 @@ export function LinkResource(props: LinkResourceProps) {
|
||||
unreads={unreads}
|
||||
contacts={contactDetails}
|
||||
api={api}
|
||||
hideAvatars={hideAvatars}
|
||||
hideNicknames={hideNicknames}
|
||||
remoteContentPolicy={remoteContentPolicy}
|
||||
editCommentId={editCommentId}
|
||||
history={props.history}
|
||||
baseUrl={`${resourceUrl}/${props.match.params.index}`}
|
||||
|
@ -1,13 +1,12 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Row, Col, Anchor, Box, Text, BaseImage, Icon, Action } from '@tlon/indigo-react';
|
||||
import { Row, Col, Anchor, Box, Text, Icon, Action } from '@tlon/indigo-react';
|
||||
|
||||
import { Sigil } from '~/logic/lib/sigil';
|
||||
import { writeText } from '~/logic/lib/util';
|
||||
import Author from '~/views/components/Author';
|
||||
|
||||
import { roleForShip } from '~/logic/lib/group';
|
||||
import { Contacts, GraphNode, Group, LocalUpdateRemoteContentPolicy, Rolodex, Unreads } from '~/types';
|
||||
import { Contacts, GraphNode, Group, Rolodex, Unreads } from '~/types';
|
||||
import GlobalApi from '~/logic/api/global';
|
||||
import { Dropdown } from '~/views/components/Dropdown';
|
||||
import RemoteContent from '~/views/components/RemoteContent';
|
||||
@ -15,9 +14,6 @@ import RemoteContent from '~/views/components/RemoteContent';
|
||||
interface LinkItemProps {
|
||||
node: GraphNode;
|
||||
resource: string;
|
||||
hideAvatars: boolean;
|
||||
hideNicknames: boolean;
|
||||
remoteContentPolicy: LocalUpdateRemoteContentPolicy;
|
||||
api: GlobalApi;
|
||||
group: Group;
|
||||
path: string;
|
||||
@ -29,9 +25,6 @@ export const LinkItem = (props: LinkItemProps) => {
|
||||
const {
|
||||
node,
|
||||
resource,
|
||||
hideAvatars,
|
||||
hideNicknames,
|
||||
remoteContentPolicy,
|
||||
api,
|
||||
group,
|
||||
path,
|
||||
@ -96,7 +89,6 @@ export const LinkItem = (props: LinkItemProps) => {
|
||||
<RemoteContent
|
||||
url={contents[1].url}
|
||||
text={contents[0].text}
|
||||
remoteContentPolicy={remoteContentPolicy}
|
||||
unfold={true}
|
||||
style={{ alignSelf: 'center' }}
|
||||
oembedProps={{
|
||||
@ -129,12 +121,9 @@ export const LinkItem = (props: LinkItemProps) => {
|
||||
|
||||
<Author
|
||||
showImage
|
||||
contacts={contacts[path]}
|
||||
contacts={contacts}
|
||||
ship={author}
|
||||
date={node.post['time-sent']}
|
||||
hideAvatars={hideAvatars}
|
||||
hideNicknames={hideNicknames}
|
||||
remoteContentPolicy={remoteContentPolicy}
|
||||
group={group}
|
||||
api={api}
|
||||
></Author>
|
||||
|
@ -1,84 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Row, Col, Anchor, Box, Text, BaseImage } from '@tlon/indigo-react';
|
||||
|
||||
import { Sigil } from '~/logic/lib/sigil';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { cite } from '~/logic/lib/util';
|
||||
import { Author } from "~/views/apps/publish/components/Author";
|
||||
|
||||
import { roleForShip } from '~/logic/lib/group';
|
||||
|
||||
export const LinkItem = (props) => {
|
||||
const {
|
||||
node,
|
||||
nickname,
|
||||
avatar,
|
||||
contacts,
|
||||
unread,
|
||||
resource,
|
||||
hideAvatars,
|
||||
hideNicknames,
|
||||
api,
|
||||
group
|
||||
} = props;
|
||||
|
||||
const URLparser = new RegExp(
|
||||
/((?:([\w\d\.-]+)\:\/\/?){1}(?:(www)\.?){0,1}(((?:[\w\d-]+\.)*)([\w\d-]+\.[\w\d]+))){1}(?:\:(\d+)){0,1}((\/(?:(?:[^\/\s\?]+\/)*))(?:([^\?\/\s#]+?(?:.[^\?\s]+){0,1}){0,1}(?:\?([^\s#]+)){0,1})){0,1}(?:#([^#\s]+)){0,1}/
|
||||
);
|
||||
|
||||
const author = node.post.author;
|
||||
const index = node.post.index.split('/')[1];
|
||||
const size = node.children ? node.children.size : 0;
|
||||
const date = node.post['time-sent'];
|
||||
const contents = node.post.contents;
|
||||
const hostname = URLparser.exec(contents[1].url) ? URLparser.exec(contents[1].url)[4] : null;
|
||||
|
||||
const showAvatar = avatar && !hideAvatars;
|
||||
const showNickname = nickname && !hideNicknames;
|
||||
|
||||
const img = showAvatar
|
||||
? <BaseImage display='inline-block' src={props.avatar} height={36} width={36} />
|
||||
: <Sigil ship={`~${author}`} size={36} color={'#' + props.color} />;
|
||||
|
||||
const baseUrl = props.baseUrl || `/~404/${resource}`;
|
||||
|
||||
const ourRole = group ? roleForShip(group, window.ship) : undefined;
|
||||
const [ship, name] = resource.split('/');
|
||||
|
||||
return (
|
||||
<Row minWidth='0' flexShrink='0' width="100%" alignItems="center" py={3} bg="white">
|
||||
{img}
|
||||
<Col minWidth='0' height="100%" width='100%' justifyContent="space-between" ml={2}>
|
||||
<Anchor
|
||||
lineHeight="tall"
|
||||
display='flex'
|
||||
style={{ textDecoration: 'none' }}
|
||||
href={contents[1].url}
|
||||
width="100%"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Text display='inline-block' overflow='hidden' style={{ textOverflow: 'ellipsis', whiteSpace: 'pre' }}>{contents[0].text}</Text>
|
||||
<Text ml="2" color="gray" display='inline-block' flexShrink='0'>{hostname} ↗</Text>
|
||||
</Anchor>
|
||||
<Row alignItems="center" width="100%">
|
||||
<Author
|
||||
contacts={contacts}
|
||||
ship={author}
|
||||
hideAvatars={hideAvatars}
|
||||
hideNicknames={hideNicknames}
|
||||
unread={unread}
|
||||
date={date}
|
||||
>
|
||||
<Link to={`${baseUrl}/${index}`}>
|
||||
<Text ml="2" color="gray">{size} comments</Text>
|
||||
</Link>
|
||||
{(ourRole === 'admin' || node.post.author === window.ship)
|
||||
&& (<Text color='red' ml='2' cursor='pointer' onClick={() => api.graph.removeNodes(`~${ship}`, name, [node.post.index])}>Delete</Text>)}
|
||||
</Author>
|
||||
</Row>
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
};
|
||||
|
@ -1,70 +0,0 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { cite } from '~/logic/lib/util';
|
||||
import RemoteContent from '~/views/components/RemoteContent';
|
||||
|
||||
import { Box, Col, Anchor, Text } from '@tlon/indigo-react';
|
||||
|
||||
import moment from 'moment';
|
||||
|
||||
const URLparser = new RegExp(
|
||||
/((?:([\w\d\.-]+)\:\/\/?){1}(?:(www)\.?){0,1}(((?:[\w\d-]+\.)*)([\w\d-]+\.[\w\d]+))){1}(?:\:(\d+)){0,1}((\/(?:(?:[^\/\s\?]+\/)*))(?:([^\?\/\s#]+?(?:.[^\?\s]+){0,1}){0,1}(?:\?([^\s#]+)){0,1})){0,1}(?:#([^#\s]+)){0,1}/
|
||||
);
|
||||
|
||||
export const LinkPreview = (props) => {
|
||||
const showNickname = props.nickname && !props.hideNicknames;
|
||||
const author = props.post.author;
|
||||
const title = props.post.contents[0].text;
|
||||
const url = props.post.contents[1].url;
|
||||
const hostname = URLparser.exec(url) ? URLparser.exec(url)[4] : null;
|
||||
|
||||
const timeSent =
|
||||
moment.unix(props.post['time-sent'] / 1000).format('hh:mm a');
|
||||
|
||||
useEffect(() => {
|
||||
return () => props.api.hark.markEachAsRead(props.association, '/', props.post.index, 'link', 'link');
|
||||
}, [props.association, props.post.index]);
|
||||
|
||||
const embed = (
|
||||
<RemoteContent
|
||||
unfold={true}
|
||||
renderUrl={false}
|
||||
url={url}
|
||||
remoteContentPolicy={props.remoteContentPolicy}
|
||||
className="mw-100"
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<Box pb='6' width='100%'>
|
||||
<Box width='100%' textAlign='center'>{embed}</Box>
|
||||
<Col flex='1 1 auto' minWidth='0' minHeight='0' pt='6'>
|
||||
<Anchor href={url}
|
||||
lineHeight="tall"
|
||||
display='flex'
|
||||
style={{ textDecoration: 'none' }}
|
||||
width='100%'
|
||||
target="_blank"
|
||||
rel="noopener noreferrer">
|
||||
<Text
|
||||
display='inline-block'
|
||||
overflow='hidden'
|
||||
style={{ textOverflow: 'ellipsis', whiteSpace: 'pre' }}
|
||||
>
|
||||
{title}
|
||||
</Text>
|
||||
<Text ml="2" color="gray" display='inline-block' flexShrink='0'>{hostname} ↗</Text>
|
||||
</Anchor>
|
||||
<Box width='100%' pt='1'>
|
||||
<Text fontSize='0' pr='2' display='inline-block' mono={!showNickname} title={author}>
|
||||
{showNickname ? props.nickname : cite(`~${author}`)}
|
||||
</Text>
|
||||
<Text fontSize='0' gray pr='3' display='inline-block'>{timeSent}</Text>
|
||||
<Text gray fontSize='0' display='inline-block'>
|
||||
{props.commentNumber} comments
|
||||
</Text>
|
||||
</Box>
|
||||
</Col>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
@ -34,9 +34,8 @@ export function ChatNotification(props: {
|
||||
contacts: Rolodex;
|
||||
groups: Groups;
|
||||
api: GlobalApi;
|
||||
remoteContentPolicy: any;
|
||||
}) {
|
||||
const { index, contents, read, time, api, timebox, remoteContentPolicy } = props;
|
||||
const { index, contents, read, time, api, timebox } = props;
|
||||
const authors = _.map(contents, "author");
|
||||
|
||||
const { chat, mention } = index;
|
||||
@ -90,7 +89,6 @@ export function ChatNotification(props: {
|
||||
contacts={groupContacts}
|
||||
fontSize='0'
|
||||
pt='2'
|
||||
remoteContentPolicy={remoteContentPolicy}
|
||||
/>
|
||||
</Link>
|
||||
);
|
||||
|
@ -25,7 +25,7 @@ import ChatMessage, {MessageWithoutSigil} from "../chat/components/ChatMessage";
|
||||
|
||||
function getGraphModuleIcon(module: string) {
|
||||
if (module === "link") {
|
||||
return "Links";
|
||||
return "Collection";
|
||||
}
|
||||
return _.capitalize(module);
|
||||
}
|
||||
@ -80,7 +80,6 @@ const GraphNodeContent = ({ group, post, contacts, mod, description, index, remo
|
||||
content={contents}
|
||||
contacts={contacts}
|
||||
group={group}
|
||||
remoteContentPolicy={remoteContentPolicy}
|
||||
/>
|
||||
}
|
||||
return null;
|
||||
@ -91,7 +90,6 @@ const GraphNodeContent = ({ group, post, contacts, mod, description, index, remo
|
||||
content={contents}
|
||||
group={group}
|
||||
contacts={contacts}
|
||||
remoteContentPolicy={remoteContentPolicy}
|
||||
/>
|
||||
} else if (idx[1] === "1") {
|
||||
const [{ text: header }, { text: body }] = contents;
|
||||
@ -132,7 +130,6 @@ const GraphNodeContent = ({ group, post, contacts, mod, description, index, remo
|
||||
msg={post}
|
||||
fontSize='0'
|
||||
pt='2'
|
||||
remoteContentPolicy={remoteContentPolicy}
|
||||
/>
|
||||
</Row>);
|
||||
|
||||
@ -219,7 +216,6 @@ const GraphNode = ({
|
||||
mod={mod}
|
||||
description={description}
|
||||
index={index}
|
||||
remoteContentPolicy={remoteContentPolicy}
|
||||
group={group}
|
||||
/>
|
||||
</Row>
|
||||
@ -239,9 +235,8 @@ export function GraphNotification(props: {
|
||||
groups: Groups;
|
||||
contacts: Rolodex;
|
||||
api: GlobalApi;
|
||||
remoteContentPolicy: any;
|
||||
}) {
|
||||
const { contents, index, read, time, api, timebox, remoteContentPolicy, groups } = props;
|
||||
const { contents, index, read, time, api, timebox, groups } = props;
|
||||
|
||||
const authors = _.map(contents, "author");
|
||||
const { graph, group } = index;
|
||||
@ -287,7 +282,6 @@ return (
|
||||
groupPath={group}
|
||||
read={read}
|
||||
onRead={onClick}
|
||||
remoteContentPolicy={remoteContentPolicy}
|
||||
/>
|
||||
))}
|
||||
</Col>
|
||||
|
@ -4,7 +4,7 @@ import f from "lodash/fp";
|
||||
import _ from "lodash";
|
||||
import moment from "moment";
|
||||
import { PropFunc } from "~/types/util";
|
||||
import { getContactDetails } from "~/logic/lib/util";
|
||||
import { getContactDetails, useShowNickname } from "~/logic/lib/util";
|
||||
import { Associations, Contact, Contacts, Rolodex } from "~/types";
|
||||
|
||||
const Text = (props: PropFunc<typeof Text>) => (
|
||||
@ -14,7 +14,7 @@ const Text = (props: PropFunc<typeof Text>) => (
|
||||
function Author(props: { patp: string; contacts: Contacts; last?: boolean }) {
|
||||
const contact: Contact | undefined = props.contacts?.[props.patp];
|
||||
|
||||
const showNickname = !!contact?.nickname;
|
||||
const showNickname = useShowNickname(contact);
|
||||
const name = contact?.nickname || `~${props.patp}`;
|
||||
|
||||
return (
|
||||
|
@ -144,7 +144,6 @@ export default function Inbox(props: {
|
||||
graphConfig={props.notificationsGraphConfig}
|
||||
groupConfig={props.notificationsGroupConfig}
|
||||
chatConfig={props.notificationsChatConfig}
|
||||
remoteContentPolicy={props.remoteContentPolicy}
|
||||
api={api}
|
||||
/>
|
||||
)}
|
||||
@ -163,7 +162,6 @@ export default function Inbox(props: {
|
||||
graphConfig={props.notificationsGraphConfig}
|
||||
groupConfig={props.notificationsGroupConfig}
|
||||
chatConfig={props.notificationsChatConfig}
|
||||
remoteContentPolicy={props.remoteContentPolicy}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
@ -229,7 +227,6 @@ function DaySection({
|
||||
contacts={contacts}
|
||||
groups={groups}
|
||||
time={date}
|
||||
remoteContentPolicy={remoteContentPolicy}
|
||||
/>
|
||||
</React.Fragment>
|
||||
))
|
||||
|
@ -31,7 +31,6 @@ interface NotificationProps {
|
||||
graphConfig: NotificationGraphConfig;
|
||||
groupConfig: GroupNotificationsConfig;
|
||||
chatConfig: string[];
|
||||
remoteContentPolicy: any;
|
||||
}
|
||||
|
||||
function getMuted(
|
||||
@ -143,7 +142,6 @@ export function Notification(props: NotificationProps) {
|
||||
timebox={props.time}
|
||||
time={time}
|
||||
associations={associations}
|
||||
remoteContentPolicy={props.remoteContentPolicy}
|
||||
/>
|
||||
</Wrapper>
|
||||
);
|
||||
@ -184,7 +182,6 @@ export function Notification(props: NotificationProps) {
|
||||
timebox={props.time}
|
||||
time={time}
|
||||
associations={associations}
|
||||
remoteContentPolicy={props.remoteContentPolicy}
|
||||
/>
|
||||
</Wrapper>
|
||||
);
|
||||
|
@ -2,6 +2,7 @@ import React, { useCallback, useState } from "react";
|
||||
import _ from 'lodash';
|
||||
import { Box, Col, Text, Row } from "@tlon/indigo-react";
|
||||
import { Link, Switch, Route } from "react-router-dom";
|
||||
import Helmet from "react-helmet";
|
||||
|
||||
import { Body } from "~/views/components/Body";
|
||||
import { PropFunc } from "~/types/util";
|
||||
@ -52,74 +53,79 @@ export default function NotificationsScreen(props: any) {
|
||||
render={(routeProps) => {
|
||||
const { view } = routeProps.match.params;
|
||||
return (
|
||||
<Body>
|
||||
<Col overflowY="hidden" height="100%">
|
||||
<Row
|
||||
p="3"
|
||||
alignItems="center"
|
||||
height="48px"
|
||||
justifyContent="space-between"
|
||||
width="100%"
|
||||
borderBottom="1"
|
||||
borderBottomColor="washedGray"
|
||||
>
|
||||
<Text>Updates</Text>
|
||||
<Row>
|
||||
<Box>
|
||||
<HeaderLink current={view} view="">
|
||||
Inbox
|
||||
</HeaderLink>
|
||||
</Box>
|
||||
<Box>
|
||||
<HeaderLink current={view} view="preferences">
|
||||
Preferences
|
||||
</HeaderLink>
|
||||
</Box>
|
||||
</Row>
|
||||
<Dropdown
|
||||
alignX="right"
|
||||
alignY="top"
|
||||
options={
|
||||
<Col
|
||||
p="2"
|
||||
backgroundColor="white"
|
||||
border={1}
|
||||
borderRadius={1}
|
||||
borderColor="lightGray"
|
||||
gapY="2"
|
||||
>
|
||||
<FormikOnBlur
|
||||
initialValues={filter}
|
||||
onSubmit={onSubmit}
|
||||
>
|
||||
<GroupSearch
|
||||
id="groups"
|
||||
label="Filter Groups"
|
||||
caption="Only show notifications from this group"
|
||||
associations={props.associations}
|
||||
/>
|
||||
</FormikOnBlur>
|
||||
</Col>
|
||||
}
|
||||
<>
|
||||
<Helmet defer={false}>
|
||||
<title>{ props.notificationsCount ? `(${String(props.notificationsCount) }) `: '' }Landscape - Notifications</title>
|
||||
</Helmet>
|
||||
<Body>
|
||||
<Col overflowY="hidden" height="100%">
|
||||
<Row
|
||||
p="3"
|
||||
alignItems="center"
|
||||
height="48px"
|
||||
justifyContent="space-between"
|
||||
width="100%"
|
||||
borderBottom="1"
|
||||
borderBottomColor="washedGray"
|
||||
>
|
||||
<Box>
|
||||
<Text mr="1" gray>
|
||||
Filter:
|
||||
</Text>
|
||||
<Text>{groupFilterDesc}</Text>
|
||||
</Box>
|
||||
</Dropdown>
|
||||
</Row>
|
||||
{view === "preferences" && (
|
||||
<NotificationPreferences
|
||||
graphConfig={props.notificationsGraphConfig}
|
||||
api={props.api}
|
||||
dnd={props.doNotDisturb}
|
||||
/>
|
||||
)}
|
||||
{!view && <Inbox {...props} filter={filter.groups} />}
|
||||
</Col>
|
||||
</Body>
|
||||
<Text>Updates</Text>
|
||||
<Row>
|
||||
<Box>
|
||||
<HeaderLink current={view} view="">
|
||||
Inbox
|
||||
</HeaderLink>
|
||||
</Box>
|
||||
<Box>
|
||||
<HeaderLink current={view} view="preferences">
|
||||
Preferences
|
||||
</HeaderLink>
|
||||
</Box>
|
||||
</Row>
|
||||
<Dropdown
|
||||
alignX="right"
|
||||
alignY="top"
|
||||
options={
|
||||
<Col
|
||||
p="2"
|
||||
backgroundColor="white"
|
||||
border={1}
|
||||
borderRadius={1}
|
||||
borderColor="lightGray"
|
||||
gapY="2"
|
||||
>
|
||||
<FormikOnBlur
|
||||
initialValues={filter}
|
||||
onSubmit={onSubmit}
|
||||
>
|
||||
<GroupSearch
|
||||
id="groups"
|
||||
label="Filter Groups"
|
||||
caption="Only show notifications from this group"
|
||||
associations={props.associations}
|
||||
/>
|
||||
</FormikOnBlur>
|
||||
</Col>
|
||||
}
|
||||
>
|
||||
<Box>
|
||||
<Text mr="1" gray>
|
||||
Filter:
|
||||
</Text>
|
||||
<Text>{groupFilterDesc}</Text>
|
||||
</Box>
|
||||
</Dropdown>
|
||||
</Row>
|
||||
{view === "preferences" && (
|
||||
<NotificationPreferences
|
||||
graphConfig={props.notificationsGraphConfig}
|
||||
api={props.api}
|
||||
dnd={props.doNotDisturb}
|
||||
/>
|
||||
)}
|
||||
{!view && <Inbox {...props} filter={filter.groups} />}
|
||||
</Col>
|
||||
</Body>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
@ -12,6 +12,7 @@ import GlobalApi from '~/logic/api/global';
|
||||
import { uxToHex } from '~/logic/lib/util';
|
||||
import { S3State, BackgroundConfig } from '~/types';
|
||||
import { BackgroundPicker, BgType } from './BackgroundPicker';
|
||||
import useLocalState, { LocalState } from '~/logic/state/local';
|
||||
|
||||
const formSchema = Yup.object().shape({
|
||||
bgType: Yup.string()
|
||||
@ -33,15 +34,13 @@ interface FormSchema {
|
||||
|
||||
interface DisplayFormProps {
|
||||
api: GlobalApi;
|
||||
dark: boolean;
|
||||
background: BackgroundConfig;
|
||||
hideAvatars: boolean;
|
||||
hideNicknames: boolean;
|
||||
s3: S3State;
|
||||
}
|
||||
|
||||
export default function DisplayForm(props: DisplayFormProps) {
|
||||
const { api, background, hideAvatars, hideNicknames, s3 } = props;
|
||||
const { api, s3 } = props;
|
||||
|
||||
const { hideAvatars, hideNicknames, background, set: setLocalState } = useLocalState();
|
||||
|
||||
let bgColor, bgUrl;
|
||||
if (background?.type === 'url') {
|
||||
@ -72,10 +71,11 @@ export default function DisplayForm(props: DisplayFormProps) {
|
||||
? { type: 'url', url: values.bgUrl || '' }
|
||||
: undefined;
|
||||
|
||||
api.local.setBackground(bgConfig);
|
||||
api.local.hideAvatars(values.avatars);
|
||||
api.local.hideNicknames(values.nicknames);
|
||||
api.local.dehydrate();
|
||||
setLocalState((state: LocalState) => {
|
||||
state.background = bgConfig;
|
||||
state.hideAvatars = values.avatars;
|
||||
state.hideNicknames = values.nicknames;
|
||||
});
|
||||
actions.setSubmitting(false);
|
||||
}}
|
||||
>
|
||||
|
@ -8,7 +8,7 @@ import { Formik, Form } from "formik";
|
||||
import * as Yup from "yup";
|
||||
|
||||
import GlobalApi from "~/logic/api/global";
|
||||
import { LocalUpdateRemoteContentPolicy } from "~/types/local-update";
|
||||
import useLocalState from "~/logic/state/local";
|
||||
|
||||
const formSchema = Yup.object().shape({
|
||||
imageShown: Yup.boolean(),
|
||||
@ -26,11 +26,12 @@ interface FormSchema {
|
||||
|
||||
interface RemoteContentFormProps {
|
||||
api: GlobalApi;
|
||||
remoteContentPolicy: LocalUpdateRemoteContentPolicy;
|
||||
}
|
||||
|
||||
export default function RemoteContentForm(props: RemoteContentFormProps) {
|
||||
const { api, remoteContentPolicy } = props;
|
||||
const { api } = props;
|
||||
const remoteContentPolicy = useLocalState(state => state.remoteContentPolicy);
|
||||
const setRemoteContentPolicy = useLocalState(state => state.set);
|
||||
const imageShown = remoteContentPolicy.imageShown;
|
||||
const audioShown = remoteContentPolicy.audioShown;
|
||||
const videoShown = remoteContentPolicy.videoShown;
|
||||
@ -47,13 +48,9 @@ export default function RemoteContentForm(props: RemoteContentFormProps) {
|
||||
} as FormSchema
|
||||
}
|
||||
onSubmit={(values, actions) => {
|
||||
api.local.setRemoteContentPolicy({
|
||||
imageShown: values.imageShown,
|
||||
audioShown: values.audioShown,
|
||||
videoShown: values.videoShown,
|
||||
oembedShown: values.oembedShown,
|
||||
setRemoteContentPolicy(state => {
|
||||
Object.assign(state.remoteContentPolicy, values);
|
||||
});
|
||||
api.local.dehydrate();
|
||||
actions.setSubmitting(false);
|
||||
}}
|
||||
>
|
||||
|
@ -13,12 +13,7 @@ type ProfileProps = StoreState & { api: GlobalApi; ship: string };
|
||||
|
||||
export default function Settings({
|
||||
api,
|
||||
s3,
|
||||
dark,
|
||||
hideAvatars,
|
||||
hideNicknames,
|
||||
background,
|
||||
remoteContentPolicy
|
||||
s3
|
||||
}: ProfileProps) {
|
||||
return (
|
||||
<Box
|
||||
@ -32,13 +27,9 @@ export default function Settings({
|
||||
>
|
||||
<DisplayForm
|
||||
api={api}
|
||||
dark={dark}
|
||||
hideNicknames={hideNicknames}
|
||||
hideAvatars={hideAvatars}
|
||||
background={background}
|
||||
s3={s3}
|
||||
/>
|
||||
<RemoteContentForm {...{ api, remoteContentPolicy }} />
|
||||
<RemoteContentForm api={api} />
|
||||
<S3Form api={api} s3={s3} />
|
||||
<SecuritySettings api={api} />
|
||||
</Box>
|
||||
|
@ -9,6 +9,7 @@ import { uxToHex, MOBILE_BROWSER_REGEX } from "~/logic/lib/util";
|
||||
|
||||
import Settings from "./components/settings";
|
||||
import { ContactCard } from "~/views/landscape/components/ContactCard";
|
||||
import useLocalState from "~/logic/state/local";
|
||||
|
||||
const SidebarItem = ({ children, view, current }) => {
|
||||
const selected = current === view;
|
||||
@ -42,10 +43,11 @@ const SidebarItem = ({ children, view, current }) => {
|
||||
|
||||
export default function ProfileScreen(props: any) {
|
||||
const { ship, dark } = props;
|
||||
const hideAvatars = useLocalState(state => state.hideAvatars);
|
||||
return (
|
||||
<>
|
||||
<Helmet defer={false}>
|
||||
<title>OS1 - Profile</title>
|
||||
<title>{ props.notificationsCount ? `(${String(props.notificationsCount) }) `: '' }Landscape - Profile</title>
|
||||
</Helmet>
|
||||
<Switch>
|
||||
<Route
|
||||
@ -65,7 +67,7 @@ export default function ProfileScreen(props: any) {
|
||||
history.replace("/~profile/identity");
|
||||
}
|
||||
|
||||
const image = (!props?.hideAvatars && contact?.avatar)
|
||||
const image = (!hideAvatars && contact?.avatar)
|
||||
? <BaseImage src={contact.avatar} width='100%' height='100%' style={{ objectFit: 'cover' }} />
|
||||
: <Sigil ship={`~${ship}`} size={80} color={sigilColor} />;
|
||||
return (
|
||||
@ -132,8 +134,6 @@ export default function ProfileScreen(props: any) {
|
||||
path="/~/default"
|
||||
api={props.api}
|
||||
s3={props.s3}
|
||||
hideAvatars={props.hideAvatars}
|
||||
hideNicknames={props.hideNicknames}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
@ -35,10 +35,7 @@ export function PublishResource(props: PublishResourceProps) {
|
||||
history={props.history}
|
||||
match={props.match}
|
||||
location={props.location}
|
||||
hideAvatars={props.hideAvatars}
|
||||
unreads={props.unreads}
|
||||
hideNicknames={props.hideNicknames}
|
||||
remoteContentPolicy={props.remoteContentPolicy}
|
||||
graphs={props.graphs}
|
||||
s3={props.s3}
|
||||
/>
|
||||
|
@ -10,7 +10,7 @@ import { NoteNavigation } from "./NoteNavigation";
|
||||
import GlobalApi from "~/logic/api/global";
|
||||
import { getLatestRevision, getComments } from '~/logic/lib/publish';
|
||||
import Author from "~/views/components/Author";
|
||||
import { Contacts, GraphNode, Graph, LocalUpdateRemoteContentPolicy, Association, Unreads, Group } from "~/types";
|
||||
import { Contacts, GraphNode, Graph, Association, Unreads, Group } from "~/types";
|
||||
|
||||
interface NoteProps {
|
||||
ship: string;
|
||||
@ -21,9 +21,6 @@ interface NoteProps {
|
||||
notebook: Graph;
|
||||
contacts: Contacts;
|
||||
api: GlobalApi;
|
||||
hideAvatars: boolean;
|
||||
hideNicknames: boolean;
|
||||
remoteContentPolicy: LocalUpdateRemoteContentPolicy;
|
||||
rootUrl: string;
|
||||
baseUrl: string;
|
||||
group: Group;
|
||||
@ -50,7 +47,7 @@ export function Note(props: NoteProps & RouteComponentProps) {
|
||||
const noteId = bigInt(index[1]);
|
||||
useEffect(() => {
|
||||
api.hark.markEachAsRead(props.association, '/',`/${index[1]}/1/1`, 'note', 'publish');
|
||||
}, [props.association]);
|
||||
}, [props.association, props.note]);
|
||||
|
||||
|
||||
|
||||
@ -78,6 +75,13 @@ export function Note(props: NoteProps & RouteComponentProps) {
|
||||
);
|
||||
}
|
||||
|
||||
const windowRef = React.useRef(null);
|
||||
useEffect(() => {
|
||||
if (windowRef.current) {
|
||||
windowRef.current.parentElement.scrollTop = 0;
|
||||
}
|
||||
}, [windowRef, note]);
|
||||
|
||||
return (
|
||||
<Box
|
||||
my={3}
|
||||
@ -89,6 +93,7 @@ export function Note(props: NoteProps & RouteComponentProps) {
|
||||
width="100%"
|
||||
gridRowGap={4}
|
||||
mx="auto"
|
||||
ref={windowRef}
|
||||
>
|
||||
<Link to={rootUrl}>
|
||||
<Text>{"<- Notebook Index"}</Text>
|
||||
@ -97,8 +102,6 @@ export function Note(props: NoteProps & RouteComponentProps) {
|
||||
<Text display="block" mb={2}>{title || ""}</Text>
|
||||
<Box display="flex">
|
||||
<Author
|
||||
hideNicknames={props?.hideNicknames}
|
||||
hideAvatars={props?.hideAvatars}
|
||||
ship={post?.author}
|
||||
contacts={contacts}
|
||||
date={post?.["time-sent"]}
|
||||
@ -123,9 +126,6 @@ export function Note(props: NoteProps & RouteComponentProps) {
|
||||
contacts={props.contacts}
|
||||
association={props.association}
|
||||
api={props.api}
|
||||
hideNicknames={props.hideNicknames}
|
||||
hideAvatars={props.hideAvatars}
|
||||
remoteContentPolicy={props.remoteContentPolicy}
|
||||
baseUrl={baseUrl}
|
||||
editCommentId={editCommentId}
|
||||
history={props.history}
|
||||
|
@ -19,8 +19,6 @@ interface NotePreviewProps {
|
||||
host: string;
|
||||
book: string;
|
||||
node: GraphNode;
|
||||
hideAvatars?: boolean;
|
||||
hideNicknames?: boolean;
|
||||
baseUrl: string;
|
||||
unreads: Unreads;
|
||||
contacts: Contacts;
|
||||
@ -33,7 +31,7 @@ const WrappedBox = styled(Box)`
|
||||
`;
|
||||
|
||||
export function NotePreview(props: NotePreviewProps) {
|
||||
const { node, contacts, hideAvatars, hideNicknames, group } = props;
|
||||
const { node, contacts, group } = props;
|
||||
const { post } = node;
|
||||
if (!post) {
|
||||
return null;
|
||||
@ -84,8 +82,6 @@ export function NotePreview(props: NotePreviewProps) {
|
||||
contacts={contacts}
|
||||
ship={post?.author}
|
||||
date={post?.['time-sent']}
|
||||
hideAvatars={hideAvatars || false}
|
||||
hideNicknames={hideNicknames || false}
|
||||
group={group}
|
||||
unread={isUnread}
|
||||
api={props.api}
|
||||
|
@ -6,7 +6,7 @@ import { RouteComponentProps } from "react-router-dom";
|
||||
import Note from "./Note";
|
||||
import { EditPost } from "./EditPost";
|
||||
|
||||
import { GraphNode, Graph, Contacts, LocalUpdateRemoteContentPolicy, Association, S3State, Group } from "~/types";
|
||||
import { GraphNode, Graph, Contacts, Association, S3State, Group } from "~/types";
|
||||
|
||||
interface NoteRoutesProps {
|
||||
ship: string;
|
||||
@ -16,9 +16,6 @@ interface NoteRoutesProps {
|
||||
notebook: Graph;
|
||||
contacts: Contacts;
|
||||
api: GlobalApi;
|
||||
remoteContentPolicy: LocalUpdateRemoteContentPolicy;
|
||||
hideNicknames: boolean;
|
||||
hideAvatars: boolean;
|
||||
association: Association;
|
||||
baseUrl?: string;
|
||||
rootUrl?: string;
|
||||
|
@ -1,13 +1,10 @@
|
||||
import React, { PureComponent } from "react";
|
||||
import { Link, RouteComponentProps, Route, Switch } from "react-router-dom";
|
||||
import React from "react";
|
||||
import { Link, RouteComponentProps } from "react-router-dom";
|
||||
import { NotebookPosts } from "./NotebookPosts";
|
||||
import { roleForShip } from "~/logic/lib/group";
|
||||
import { Box, Button, Text, Row, Col } from "@tlon/indigo-react";
|
||||
import GlobalApi from "~/logic/api/global";
|
||||
import styled from "styled-components";
|
||||
import { Contacts, Rolodex, Groups, Associations, Graph, Association, Unreads } from "~/types";
|
||||
import { deSig } from "~/logic/lib/util";
|
||||
import { StatelessAsyncButton } from "~/views/components/StatelessAsyncButton";
|
||||
import { useShowNickname } from "~/logic/lib/util";
|
||||
|
||||
interface NotebookProps {
|
||||
api: GlobalApi;
|
||||
@ -19,26 +16,17 @@ interface NotebookProps {
|
||||
associations: Associations;
|
||||
contacts: Rolodex;
|
||||
groups: Groups;
|
||||
hideNicknames: boolean;
|
||||
hideAvatars: boolean;
|
||||
baseUrl: string;
|
||||
rootUrl: string;
|
||||
unreads: Unreads;
|
||||
}
|
||||
|
||||
interface NotebookState {
|
||||
isUnsubscribing: boolean;
|
||||
tab: string;
|
||||
}
|
||||
|
||||
export function Notebook(props: NotebookProps & RouteComponentProps) {
|
||||
const {
|
||||
ship,
|
||||
book,
|
||||
notebookContacts,
|
||||
groups,
|
||||
hideNicknames,
|
||||
hideAvatars,
|
||||
association,
|
||||
graph
|
||||
} = props;
|
||||
@ -46,7 +34,7 @@ export function Notebook(props: NotebookProps & RouteComponentProps) {
|
||||
|
||||
const group = groups[association?.['group-path']];
|
||||
if (!group) {
|
||||
return null; // Waitin on groups to populate
|
||||
return null; // Waiting on groups to populate
|
||||
}
|
||||
|
||||
const relativePath = (p: string) => props.baseUrl + p;
|
||||
@ -59,7 +47,7 @@ export function Notebook(props: NotebookProps & RouteComponentProps) {
|
||||
isWriter = isOwn || group.tags?.publish?.[`writers-${book}`]?.has(window.ship);
|
||||
}
|
||||
|
||||
const showNickname = contact?.nickname && !hideNicknames;
|
||||
const showNickname = useShowNickname(contact);
|
||||
|
||||
return (
|
||||
<Col gapY="4" pt={4} mx="auto" px={3} maxWidth="768px">
|
||||
@ -85,9 +73,7 @@ export function Notebook(props: NotebookProps & RouteComponentProps) {
|
||||
host={ship}
|
||||
book={book}
|
||||
contacts={notebookContacts ? notebookContacts : {}}
|
||||
hideNicknames={hideNicknames}
|
||||
unreads={props.unreads}
|
||||
hideAvatars={hideAvatars}
|
||||
baseUrl={props.baseUrl}
|
||||
api={props.api}
|
||||
group={group}
|
||||
|
@ -8,13 +8,11 @@ import {
|
||||
Groups,
|
||||
Contacts,
|
||||
Rolodex,
|
||||
LocalUpdateRemoteContentPolicy,
|
||||
Unreads,
|
||||
S3State
|
||||
} from "~/types";
|
||||
import { Center, LoadingSpinner } from "@tlon/indigo-react";
|
||||
import { Notebook as INotebook } from "~/types/publish-update";
|
||||
import bigInt, { BigInteger } from 'big-integer';
|
||||
import bigInt from 'big-integer';
|
||||
|
||||
import Notebook from "./Notebook";
|
||||
import NewPost from "./new-post";
|
||||
@ -33,10 +31,7 @@ interface NotebookRoutesProps {
|
||||
groups: Groups;
|
||||
baseUrl: string;
|
||||
rootUrl: string;
|
||||
hideAvatars: boolean;
|
||||
hideNicknames: boolean;
|
||||
association: Association;
|
||||
remoteContentPolicy: LocalUpdateRemoteContentPolicy;
|
||||
associations: Associations;
|
||||
s3: S3State;
|
||||
}
|
||||
@ -116,9 +111,6 @@ export function NotebookRoutes(
|
||||
noteId={noteIdNum}
|
||||
contacts={notebookContacts}
|
||||
association={props.association}
|
||||
hideAvatars={props.hideAvatars}
|
||||
hideNicknames={props.hideNicknames}
|
||||
remoteContentPolicy={props.remoteContentPolicy}
|
||||
group={group}
|
||||
s3={props.s3}
|
||||
{...routeProps}
|
||||
|
@ -46,8 +46,8 @@ export default class TermApp extends Component {
|
||||
render() {
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>OS1 - Terminal</title>
|
||||
<Helmet defer={false}>
|
||||
<title>{ this.props.notificationsCount ? `(${String(this.props.notificationsCount) }) `: '' }Landscape</title>
|
||||
</Helmet>
|
||||
<Box
|
||||
height='100%'
|
||||
|
@ -2,7 +2,7 @@ import React, {ReactNode} from "react";
|
||||
import moment from "moment";
|
||||
import { Row, Box } from "@tlon/indigo-react";
|
||||
|
||||
import { uxToHex, cite } from "~/logic/lib/util";
|
||||
import { uxToHex, cite, useShowNickname } from "~/logic/lib/util";
|
||||
import { Contacts, Rolodex } from "~/types/contact-update";
|
||||
import OverlaySigil from "./OverlaySigil";
|
||||
import { Group, Association } from "~/types";
|
||||
@ -14,8 +14,6 @@ interface AuthorProps {
|
||||
ship: string;
|
||||
date: number;
|
||||
showImage?: boolean;
|
||||
hideAvatars: boolean;
|
||||
hideNicknames: boolean;
|
||||
children?: ReactNode;
|
||||
unread?: boolean;
|
||||
group: Group;
|
||||
@ -23,16 +21,16 @@ interface AuthorProps {
|
||||
}
|
||||
|
||||
export default function Author(props: AuthorProps) {
|
||||
const { contacts, ship = '', date, showImage, hideAvatars, hideNicknames, group, api } = props;
|
||||
const { contacts, ship = '', date, showImage, group, api } = props;
|
||||
const history = useHistory();
|
||||
let contact;
|
||||
if (contacts) {
|
||||
contact = ship in contacts ? contacts[ship] : null;
|
||||
}
|
||||
const color = contact?.color ? `#${uxToHex(contact?.color)}` : "#000000";
|
||||
const showNickname = !props.hideNicknames && contact?.nickname;
|
||||
const showNickname = useShowNickname(contact);
|
||||
|
||||
const name = showNickname ? contact?.nickname : cite(ship);
|
||||
const name = showNickname ? contact.nickname : cite(ship);
|
||||
const dateFmt = moment(date).fromNow();
|
||||
return (
|
||||
<Row alignItems="center" width="auto">
|
||||
@ -44,8 +42,6 @@ export default function Author(props: AuthorProps) {
|
||||
color={color}
|
||||
sigilClass={''}
|
||||
group={group}
|
||||
hideAvatars={hideAvatars}
|
||||
hideNicknames={hideNicknames}
|
||||
history={history}
|
||||
api={api}
|
||||
bg="white"
|
||||
|
@ -7,7 +7,7 @@ import styled from 'styled-components';
|
||||
import Author from '~/views/components/Author';
|
||||
import { GraphNode, TextContent } from '~/types/graph-update';
|
||||
import tokenizeMessage from '~/logic/lib/tokenizeMessage';
|
||||
import { LocalUpdateRemoteContentPolicy, Group } from '~/types';
|
||||
import { Group } from '~/types';
|
||||
import { MentionText } from '~/views/components/MentionText';
|
||||
import { getLatestCommentRevision } from '~/logic/lib/publish';
|
||||
|
||||
@ -25,14 +25,11 @@ interface CommentItemProps {
|
||||
name: string;
|
||||
ship: string;
|
||||
api: GlobalApi;
|
||||
hideNicknames: boolean;
|
||||
hideAvatars: boolean;
|
||||
remoteContentPolicy: LocalUpdateRemoteContentPolicy;
|
||||
group: Group;
|
||||
}
|
||||
|
||||
export function CommentItem(props: CommentItemProps) {
|
||||
const { ship, contacts, name, api, remoteContentPolicy, comment, group } = props;
|
||||
const { ship, contacts, name, api, comment, group } = props;
|
||||
const [revNum, post] = getLatestCommentRevision(comment);
|
||||
const disabled = props.pending || window.ship !== post?.author;
|
||||
|
||||
@ -53,9 +50,6 @@ export function CommentItem(props: CommentItemProps) {
|
||||
ship={post?.author}
|
||||
date={post?.['time-sent']}
|
||||
unread={props.unread}
|
||||
hideAvatars={props.hideAvatars}
|
||||
hideNicknames={props.hideNicknames}
|
||||
remoteContentPolicy={remoteContentPolicy}
|
||||
group={group}
|
||||
api={api}
|
||||
>
|
||||
@ -81,9 +75,6 @@ export function CommentItem(props: CommentItemProps) {
|
||||
contacts={contacts}
|
||||
group={group}
|
||||
content={post?.contents}
|
||||
remoteContentPolicy={remoteContentPolicy}
|
||||
hideNicknames={props.hideNicknames}
|
||||
hideAvatars={props.hideAvatars}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
|
@ -6,7 +6,7 @@ import CommentInput from './CommentInput';
|
||||
import { Contacts } from '~/types/contact-update';
|
||||
import GlobalApi from '~/logic/api/global';
|
||||
import { FormikHelpers } from 'formik';
|
||||
import { Group, GraphNode, LocalUpdateRemoteContentPolicy, Unreads, Association } from '~/types';
|
||||
import { Group, GraphNode, Association } from '~/types';
|
||||
import { createPost, createBlankNodeWithChildPost } from '~/logic/api/graph';
|
||||
import { getLatestCommentRevision } from '~/logic/lib/publish';
|
||||
import { scanForMentions } from '~/logic/lib/graph';
|
||||
@ -21,9 +21,6 @@ interface CommentsProps {
|
||||
baseUrl: string;
|
||||
contacts: Contacts;
|
||||
api: GlobalApi;
|
||||
hideAvatars: boolean;
|
||||
hideNicknames: boolean;
|
||||
remoteContentPolicy: LocalUpdateRemoteContentPolicy;
|
||||
group: Group;
|
||||
}
|
||||
|
||||
@ -80,7 +77,7 @@ export function Comments(props: CommentsProps) {
|
||||
if ('text' in curr) {
|
||||
val = val + curr.text;
|
||||
} else if ('mention' in curr) {
|
||||
val = val + curr.mention;
|
||||
val = val + `~${curr.mention}`;
|
||||
} else if ('url' in curr) {
|
||||
val = val + curr.url;
|
||||
} else if ('code' in curr) {
|
||||
@ -126,9 +123,6 @@ export function Comments(props: CommentsProps) {
|
||||
name={name}
|
||||
ship={ship}
|
||||
unread={i >= readCount}
|
||||
hideNicknames={props.hideNicknames}
|
||||
hideAvatars={props.hideAvatars}
|
||||
remoteContentPolicy={props.remoteContentPolicy}
|
||||
baseUrl={props.baseUrl}
|
||||
group={group}
|
||||
pending={idx.toString() === props.editCommentId}
|
||||
|
@ -5,47 +5,33 @@ import {
|
||||
Contact,
|
||||
Contacts,
|
||||
Content,
|
||||
LocalUpdateRemoteContentPolicy,
|
||||
Group,
|
||||
} from "~/types";
|
||||
import RichText from "~/views/components/RichText";
|
||||
import { cite, uxToHex } from "~/logic/lib/util";
|
||||
import { ProfileOverlay } from "./ProfileOverlay";
|
||||
import {useHistory} from "react-router-dom";
|
||||
import { cite, useShowNickname, uxToHex } from "~/logic/lib/util";
|
||||
import ProfileOverlay from "./ProfileOverlay";
|
||||
import { useHistory } from "react-router-dom";
|
||||
|
||||
interface MentionTextProps {
|
||||
contact?: Contact;
|
||||
contacts?: Contacts;
|
||||
content: Content[];
|
||||
group: Group;
|
||||
remoteContentPolicy: LocalUpdateRemoteContentPolicy;
|
||||
hideNicknames: boolean;
|
||||
hideAvatars: boolean;
|
||||
}
|
||||
export function MentionText(props: MentionTextProps) {
|
||||
const { content, contacts, contact, group, hideNicknames, hideAvatars } = props;
|
||||
const { content, contacts, contact, group } = props;
|
||||
|
||||
return (
|
||||
<>
|
||||
{_.map(content, (c, idx) => {
|
||||
<RichText contacts={contacts} contact={contact} group={group}>
|
||||
{content.reduce((accum, c) => {
|
||||
if ("text" in c) {
|
||||
return (
|
||||
<RichText
|
||||
inline
|
||||
key={idx}
|
||||
remoteContentPolicy={props.remoteContentPolicy}
|
||||
>
|
||||
{c.text}
|
||||
</RichText>
|
||||
);
|
||||
return accum + c.text;
|
||||
} else if ("mention" in c) {
|
||||
return (
|
||||
<Mention key={idx} contacts={contacts || {}} contact={contact || {}} group={group} ship={c.mention} hideNicknames={hideNicknames} hideAvatars={hideAvatars} />
|
||||
);
|
||||
return accum + `[~${c.mention}]`;
|
||||
}
|
||||
return null;
|
||||
})}
|
||||
</>
|
||||
return accum;
|
||||
}, '')}
|
||||
</RichText>
|
||||
);
|
||||
}
|
||||
|
||||
@ -54,15 +40,14 @@ export function Mention(props: {
|
||||
contact: Contact;
|
||||
contacts?: Contacts;
|
||||
group: Group;
|
||||
hideNicknames: boolean;
|
||||
hideAvatars: boolean;
|
||||
}) {
|
||||
const { contacts, ship, hideNicknames, hideAvatars } = props;
|
||||
const { contacts, ship } = props;
|
||||
let { contact } = props;
|
||||
|
||||
contact = (contact?.nickname) ? contact : contacts?.[ship];
|
||||
|
||||
const showNickname = (Boolean(contact?.nickname) && !hideNicknames);
|
||||
const showNickname = useShowNickname(contact);
|
||||
|
||||
const name = showNickname ? contact?.nickname : cite(ship);
|
||||
const [showOverlay, setShowOverlay] = useState(false);
|
||||
const onDismiss = useCallback(() => {
|
||||
@ -89,8 +74,6 @@ export function Mention(props: {
|
||||
color={`#${uxToHex(contact?.color ?? '0x0')}`}
|
||||
group={group}
|
||||
onDismiss={onDismiss}
|
||||
hideAvatars={hideAvatars || false}
|
||||
hideNicknames={hideNicknames}
|
||||
history={history}
|
||||
/>
|
||||
)}
|
||||
|
@ -3,12 +3,10 @@ import React, { PureComponent } from 'react';
|
||||
import { Sigil } from '~/logic/lib/sigil';
|
||||
import { Contact, Group } from '~/types';
|
||||
|
||||
import {
|
||||
ProfileOverlay,
|
||||
OVERLAY_HEIGHT
|
||||
} from './ProfileOverlay';
|
||||
import ProfileOverlay, { OVERLAY_HEIGHT } from './ProfileOverlay';
|
||||
|
||||
import { Box, BaseImage, ColProps } from '@tlon/indigo-react';
|
||||
import { withLocalState } from '~/logic/state/local';
|
||||
|
||||
type OverlaySigilProps = ColProps & {
|
||||
ship: string;
|
||||
@ -16,12 +14,11 @@ type OverlaySigilProps = ColProps & {
|
||||
color: string;
|
||||
sigilClass: string;
|
||||
group?: Group;
|
||||
hideAvatars: boolean;
|
||||
hideNicknames: boolean;
|
||||
scrollWindow?: HTMLElement;
|
||||
history: any;
|
||||
api: any;
|
||||
className: string;
|
||||
hideAvatars: boolean;
|
||||
}
|
||||
|
||||
interface OverlaySigilState {
|
||||
@ -30,7 +27,7 @@ interface OverlaySigilState {
|
||||
bottomSpace: number | 'auto';
|
||||
}
|
||||
|
||||
export default class OverlaySigil extends PureComponent<OverlaySigilProps, OverlaySigilState> {
|
||||
class OverlaySigil extends PureComponent<OverlaySigilProps, OverlaySigilState> {
|
||||
public containerRef: React.Ref<HTMLDivElement>;
|
||||
|
||||
constructor(props) {
|
||||
@ -89,11 +86,10 @@ export default class OverlaySigil extends PureComponent<OverlaySigilProps, Overl
|
||||
contact,
|
||||
color,
|
||||
group,
|
||||
hideAvatars,
|
||||
hideNicknames,
|
||||
history,
|
||||
api,
|
||||
sigilClass,
|
||||
hideAvatars,
|
||||
...rest
|
||||
} = this.props;
|
||||
|
||||
@ -127,8 +123,6 @@ export default class OverlaySigil extends PureComponent<OverlaySigilProps, Overl
|
||||
bottomSpace={state.bottomSpace}
|
||||
group={group}
|
||||
onDismiss={this.profileHide}
|
||||
hideAvatars={hideAvatars}
|
||||
hideNicknames={hideNicknames}
|
||||
history={history}
|
||||
api={api}
|
||||
{...rest}
|
||||
@ -139,3 +133,5 @@ export default class OverlaySigil extends PureComponent<OverlaySigilProps, Overl
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withLocalState(OverlaySigil, ['hideAvatars']);
|
@ -1,10 +1,11 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
|
||||
import { Contact, Group } from '~/types';
|
||||
import { cite } from '~/logic/lib/util';
|
||||
import { cite, useShowNickname } from '~/logic/lib/util';
|
||||
import { Sigil } from '~/logic/lib/sigil';
|
||||
|
||||
import { Box, Col, Button, Text, BaseImage, ColProps } from '@tlon/indigo-react';
|
||||
import { withLocalState } from '~/logic/state/local';
|
||||
|
||||
export const OVERLAY_HEIGHT = 250;
|
||||
|
||||
@ -22,7 +23,7 @@ type ProfileOverlayProps = ColProps & {
|
||||
api: any;
|
||||
}
|
||||
|
||||
export class ProfileOverlay extends PureComponent<ProfileOverlayProps, {}> {
|
||||
class ProfileOverlay extends PureComponent<ProfileOverlayProps, {}> {
|
||||
public popoverRef: React.Ref<typeof Col>;
|
||||
|
||||
constructor(props) {
|
||||
@ -60,8 +61,8 @@ export class ProfileOverlay extends PureComponent<ProfileOverlayProps, {}> {
|
||||
topSpace,
|
||||
bottomSpace,
|
||||
group = false,
|
||||
hideNicknames,
|
||||
hideAvatars,
|
||||
hideNicknames,
|
||||
history,
|
||||
onDismiss,
|
||||
...rest
|
||||
@ -90,7 +91,7 @@ export class ProfileOverlay extends PureComponent<ProfileOverlayProps, {}> {
|
||||
classes="brt2"
|
||||
svgClass="brt2"
|
||||
/>;
|
||||
const showNickname = contact?.nickname && !hideNicknames;
|
||||
const showNickname = useShowNickname(contact, hideNicknames);
|
||||
|
||||
// TODO: we need to rethink this "top-level profile view" of other ships
|
||||
/* if (!group.hidden) {
|
||||
@ -147,3 +148,5 @@ export class ProfileOverlay extends PureComponent<ProfileOverlayProps, {}> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withLocalState(ProfileOverlay, ['hideAvatars', 'hideNicknames']);
|
@ -8,14 +8,15 @@ const ReconnectButton = ({ connection, subscription }) => {
|
||||
if (connectedStatus === "disconnected") {
|
||||
return (
|
||||
<Button onClick={reconnect} borderColor='red' px='2'>
|
||||
<Text textAlign='middle' color='red'>Reconnect ↻</Text>
|
||||
<Text display={['none', 'inline']} textAlign='middle' color='red'>Reconnect</Text>
|
||||
<Text color='red'> ↻</Text>
|
||||
</Button>
|
||||
);
|
||||
} else if (connectedStatus === "reconnecting") {
|
||||
return (
|
||||
<Button borderColor='yellow' px='2' onClick={() => {}} cursor='default'>
|
||||
<LoadingSpinner pr='2' foreground='scales.yellow60' background='scales.yellow30'/>
|
||||
<Text textAlign='middle' color='yellow'>Reconnecting</Text>
|
||||
<LoadingSpinner pr={['0','2']} foreground='scales.yellow60' background='scales.yellow30'/>
|
||||
<Text display={['none', 'inline']} textAlign='middle' color='yellow'>Reconnecting</Text>
|
||||
</Button>
|
||||
)
|
||||
} else {
|
||||
|
@ -1,16 +1,16 @@
|
||||
import React, { PureComponent, Fragment } from 'react';
|
||||
import { LocalUpdateRemoteContentPolicy } from "~/types/local-update";
|
||||
import { BaseAnchor, BaseImage, Box, Button, Text } from '@tlon/indigo-react';
|
||||
import { hasProvider } from 'oembed-parser';
|
||||
import EmbedContainer from 'react-oembed-container';
|
||||
import { memoize } from 'lodash';
|
||||
import { withLocalState } from '~/logic/state/local';
|
||||
import { RemoteContentPolicy } from '~/types/local-update';
|
||||
|
||||
interface RemoteContentProps {
|
||||
url: string;
|
||||
text?: string;
|
||||
remoteContentPolicy: LocalUpdateRemoteContentPolicy;
|
||||
unfold?: boolean;
|
||||
renderUrl?: boolean;
|
||||
remoteContentPolicy: RemoteContentPolicy;
|
||||
imageProps?: any;
|
||||
audioProps?: any;
|
||||
videoProps?: any;
|
||||
@ -29,7 +29,7 @@ const IMAGE_REGEX = new RegExp(/(jpg|img|png|gif|tiff|jpeg|webp|webm|svg)$/i);
|
||||
const AUDIO_REGEX = new RegExp(/(mp3|wav|ogg)$/i);
|
||||
const VIDEO_REGEX = new RegExp(/(mov|mp4|ogv)$/i);
|
||||
|
||||
export default class RemoteContent extends PureComponent<RemoteContentProps, RemoteContentState> {
|
||||
class RemoteContent extends PureComponent<RemoteContentProps, RemoteContentState> {
|
||||
private fetchController: AbortController | undefined;
|
||||
constructor(props) {
|
||||
super(props);
|
||||
@ -200,3 +200,5 @@ export default class RemoteContent extends PureComponent<RemoteContentProps, Rem
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default withLocalState(RemoteContent, ['remoteContentPolicy']);
|
@ -3,8 +3,11 @@ import RemoteContent from '~/views/components/RemoteContent';
|
||||
import { hasProvider } from 'oembed-parser';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import RemarkDisableTokenizers from 'remark-disable-tokenizers';
|
||||
|
||||
import { BaseAnchor, Text } from '@tlon/indigo-react';
|
||||
import { isValidPatp } from 'urbit-ob';
|
||||
|
||||
import { deSig } from '~/logic/lib/util';
|
||||
import { Mention } from '~/views/components/MentionText';
|
||||
|
||||
const DISABLED_BLOCK_TOKENS = [
|
||||
'indentedCode',
|
||||
@ -19,15 +22,31 @@ const DISABLED_BLOCK_TOKENS = [
|
||||
|
||||
const DISABLED_INLINE_TOKENS = [];
|
||||
|
||||
const RichText = React.memo(({ remoteContentPolicy, ...props }) => (
|
||||
const RichText = React.memo(({ disableRemoteContent, ...props }) => (
|
||||
<ReactMarkdown
|
||||
{...props}
|
||||
renderers={{
|
||||
link: (props) => {
|
||||
if (hasProvider(props.href)) {
|
||||
return <RemoteContent className="mw-100" url={props.href} remoteContentPolicy={remoteContentPolicy} />;
|
||||
link: (linkProps) => {
|
||||
if (disableRemoteContent) {
|
||||
linkProps.remoteContentPolicy = {
|
||||
imageShown: false,
|
||||
audioShown: false,
|
||||
videoShown: false,
|
||||
oembedShown: false
|
||||
};
|
||||
}
|
||||
return <BaseAnchor target='_blank' rel='noreferrer noopener' borderBottom='1px solid' {...props}>{props.children}</BaseAnchor>;
|
||||
if (hasProvider(linkProps.href)) {
|
||||
return <RemoteContent className="mw-100" url={linkProps.href} />;
|
||||
}
|
||||
|
||||
return <BaseAnchor target='_blank' rel='noreferrer noopener' borderBottom='1px solid' {...linkProps}>{linkProps.children}</BaseAnchor>;
|
||||
},
|
||||
linkReference: (linkProps) => {
|
||||
const linkText = String(linkProps.children[0].props.children);
|
||||
if (isValidPatp(linkText)) {
|
||||
return <Mention contacts={props.contacts || {}} contact={props.contact || {}} group={props.group} ship={deSig(linkText)} />;
|
||||
}
|
||||
return linkText;
|
||||
},
|
||||
paragraph: (paraProps) => {
|
||||
return <Text display={props.inline ? 'inline' : 'block'} mb='2' {...props}>{paraProps.children}</Text>;
|
||||
|
@ -64,6 +64,8 @@ export function ShipSearch(props: InviteSearchProps) {
|
||||
if(valid) {
|
||||
setInputShip(ship);
|
||||
setError(error === INVALID_SHIP_ERR ? undefined : error);
|
||||
} else if (ship === undefined) {
|
||||
return;
|
||||
} else {
|
||||
setError(INVALID_SHIP_ERR);
|
||||
setInputTouched(false);
|
||||
@ -190,9 +192,9 @@ export function ShipSearch(props: InviteSearchProps) {
|
||||
alignItems="center"
|
||||
py={1}
|
||||
px={2}
|
||||
border={1}
|
||||
borderColor="washedGrey"
|
||||
color="black"
|
||||
borderRadius='2'
|
||||
bg='washedGray'
|
||||
fontSize={0}
|
||||
mt={2}
|
||||
mr={2}
|
||||
|
@ -4,11 +4,12 @@ import { Row, Box, Text, Icon, Button } from '@tlon/indigo-react';
|
||||
import ReconnectButton from './ReconnectButton';
|
||||
import { StatusBarItem } from './StatusBarItem';
|
||||
import { Sigil } from '~/logic/lib/sigil';
|
||||
import useLocalState from '~/logic/state/local';
|
||||
|
||||
const StatusBar = (props) => {
|
||||
const invites = [].concat(...Object.values(props.invites).map(obj => Object.values(obj)));
|
||||
const metaKey = (window.navigator.platform.includes('Mac')) ? '⌘' : 'Ctrl+';
|
||||
|
||||
const toggleOmnibox = useLocalState(state => state.toggleOmnibox);
|
||||
return (
|
||||
<Box
|
||||
display='grid'
|
||||
@ -24,7 +25,7 @@ const StatusBar = (props) => {
|
||||
<Icon icon='Spaces' color='black'/>
|
||||
</Button>
|
||||
|
||||
<StatusBarItem mr={2} onClick={() => props.api.local.setOmnibox()}>
|
||||
<StatusBarItem mr={2} onClick={() => toggleOmnibox()}>
|
||||
{ !props.doNotDisturb && (props.notificationsCount > 0 || invites.length > 0) &&
|
||||
(<Box display="block" right="-8px" top="-8px" position="absolute" >
|
||||
<Icon color="blue" icon="Bullet" />
|
||||
@ -44,6 +45,20 @@ const StatusBar = (props) => {
|
||||
/>
|
||||
</Row>
|
||||
<Row justifyContent="flex-end" collapse>
|
||||
<StatusBarItem
|
||||
mr='2'
|
||||
backgroundColor='yellow'
|
||||
display={process.env.LANDSCAPE_STREAM === 'development' ? 'flex' : 'none'}
|
||||
justifyContent="flex-end"
|
||||
flexShrink='0'
|
||||
onClick={() => window.open(
|
||||
'https://github.com/urbit/landscape/issues/new' +
|
||||
'?assignees=&labels=development-stream&title=&' +
|
||||
`body=commit:%20urbit/urbit@${process.env.LANDSCAPE_SHORTHASH}`
|
||||
)}
|
||||
>
|
||||
<Text color='#000000'>Submit <Text color='#000000' display={['none', 'inline']}>an</Text> issue</Text>
|
||||
</StatusBarItem>
|
||||
<StatusBarItem px={'2'} flexShrink='0' onClick={() => props.history.push('/~profile')}>
|
||||
<Sigil ship={props.ship} size={16} color='black' classes='mix-blend-diff' icon />
|
||||
<Text ml={2} display={["none", "inline"]} fontFamily="mono">~{props.ship}</Text>
|
||||
|
@ -5,6 +5,7 @@ import index from '~/logic/lib/omnibox';
|
||||
import Mousetrap from 'mousetrap';
|
||||
import OmniboxInput from './OmniboxInput';
|
||||
import OmniboxResult from './OmniboxResult';
|
||||
import { withLocalState } from '~/logic/state/local';
|
||||
|
||||
import defaultApps from '~/logic/lib/default-apps';
|
||||
|
||||
@ -39,7 +40,7 @@ export class Omnibox extends Component {
|
||||
}
|
||||
|
||||
if (prevProps && this.props.show && prevProps.show !== this.props.show) {
|
||||
Mousetrap.bind('escape', () => this.props.api.local.setOmnibox());
|
||||
Mousetrap.bind('escape', this.props.toggle);
|
||||
document.addEventListener('mousedown', this.handleClickOutside);
|
||||
const touchstart = new Event('touchstart');
|
||||
this.omniInput.input.dispatchEvent(touchstart);
|
||||
@ -63,7 +64,7 @@ export class Omnibox extends Component {
|
||||
if (this.state.query.length > 0) {
|
||||
this.setState({ query: '', results: this.initialResults(), selected: [] });
|
||||
} else if (this.props.show) {
|
||||
this.props.api.local.setOmnibox();
|
||||
this.props.toggleOmnibox();
|
||||
}
|
||||
};
|
||||
|
||||
@ -96,7 +97,7 @@ export class Omnibox extends Component {
|
||||
handleClickOutside(evt) {
|
||||
if (this.props.show && !this.omniBox.contains(evt.target)) {
|
||||
this.setState({ results: this.initialResults(), query: '', selected: [] }, () => {
|
||||
this.props.api.local.setOmnibox();
|
||||
this.props.toggleOmnibox();
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -116,7 +117,7 @@ export class Omnibox extends Component {
|
||||
navigate(app, link) {
|
||||
const { props } = this;
|
||||
this.setState({ results: this.initialResults(), query: '' }, () => {
|
||||
props.api.local.setOmnibox();
|
||||
props.toggleOmnibox();
|
||||
if (defaultApps.includes(app.toLowerCase())
|
||||
|| app === 'profile'
|
||||
|| app === 'Links'
|
||||
@ -233,10 +234,9 @@ export class Omnibox extends Component {
|
||||
.filter(category => category.categoryResults.length > 0)
|
||||
.map(({ category, categoryResults }, i) => {
|
||||
const categoryTitle = (category === 'other')
|
||||
? null : <Text gray ml={2}>{category.charAt(0).toUpperCase() + category.slice(1)}</Text>;
|
||||
? null : <Row pl='2' height='5' alignItems='center' bg='washedGray'><Text gray bold>{category.charAt(0).toUpperCase() + category.slice(1)}</Text></Row>;
|
||||
const selected = this.state.selected?.length ? this.state.selected[1] : '';
|
||||
return (<Box key={i} width='max(50vw, 300px)' maxWidth='600px'>
|
||||
<Rule borderTopWidth="0.5px" color="washedGray" />
|
||||
{categoryTitle}
|
||||
{categoryResults.map((result, i2) => (
|
||||
<OmniboxResult
|
||||
@ -263,6 +263,7 @@ export class Omnibox extends Component {
|
||||
if (state?.selected?.length === 0 && Array.from(this.state.results.values()).flat().length) {
|
||||
this.setNextSelected();
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
backgroundColor='scales.black30'
|
||||
@ -299,4 +300,4 @@ export class Omnibox extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
export default withRouter(Omnibox);
|
||||
export default withRouter(withLocalState(Omnibox, ['toggleOmnibox', 'omniboxShown']));
|
||||
|
@ -37,7 +37,7 @@ export class OmniboxResult extends Component {
|
||||
|| icon.toLowerCase() === 'links'
|
||||
|| icon.toLowerCase() === 'terminal')
|
||||
{
|
||||
icon = (icon === 'Link') ? 'Links' :
|
||||
icon = (icon === 'Link') ? 'Collection' :
|
||||
(icon === 'Terminal') ? 'Dojo' : icon;
|
||||
graphic = <Icon display="inline-block" verticalAlign="middle" icon={icon} mr='2' size='16px' color={iconFill} />;
|
||||
} else if (icon === 'inbox') {
|
||||
|
@ -63,8 +63,10 @@ export function ChannelMenu(props: ChannelMenuProps) {
|
||||
}, [api, association]);
|
||||
|
||||
const onDelete = useCallback(async () => {
|
||||
await api.graph.deleteGraph(name);
|
||||
history.push(`/~landscape${workspace}`);
|
||||
if (confirm('Are you sure you want to delete this channel?')) {
|
||||
await api.graph.deleteGraph(name);
|
||||
history.push(`/~landscape${workspace}`);
|
||||
}
|
||||
}, [api, association]);
|
||||
|
||||
return (
|
||||
|
@ -19,6 +19,7 @@ import { ColorInput } from "~/views/components/ColorInput";
|
||||
import GlobalApi from "~/logic/api/global";
|
||||
import { ImageInput } from "~/views/components/ImageInput";
|
||||
import { S3State } from "~/types";
|
||||
import useLocalState from "~/logic/state/local";
|
||||
|
||||
interface ContactCardProps {
|
||||
contact: Contact;
|
||||
@ -26,8 +27,6 @@ interface ContactCardProps {
|
||||
api: GlobalApi;
|
||||
s3: S3State;
|
||||
rootIdentity: Contact;
|
||||
hideAvatars: boolean;
|
||||
hideNicknames: boolean;
|
||||
}
|
||||
|
||||
const formSchema = Yup.object({
|
||||
@ -72,6 +71,9 @@ const emptyContact = {
|
||||
};
|
||||
|
||||
export function ContactCard(props: ContactCardProps) {
|
||||
const { hideAvatars, hideNicknames } = useLocalState(({ hideAvatars, hideNicknames }) => ({
|
||||
hideAvatars, hideNicknames
|
||||
}));
|
||||
const us = `~${window.ship}`;
|
||||
const { contact, rootIdentity } = props;
|
||||
const onSubmit = async (values: any, actions: FormikHelpers<Contact>) => {
|
||||
@ -114,11 +116,11 @@ export function ContactCard(props: ContactCardProps) {
|
||||
};
|
||||
|
||||
const hexColor = contact?.color ? `#${uxToHex(contact.color)}` : "#000000";
|
||||
const image = (!props?.hideAvatars && contact?.avatar)
|
||||
const image = (!hideAvatars && contact?.avatar)
|
||||
? <BaseImage src={contact.avatar} width='100%' height='100%' style={{ objectFit: 'cover' }} />
|
||||
: <Sigil ship={us} size={32} color={hexColor} />;
|
||||
|
||||
const nickname = (!props.hideNicknames && contact?.nickname) ? contact.nickname : "";
|
||||
const nickname = (!hideNicknames && contact?.nickname) ? contact.nickname : "";
|
||||
|
||||
return (
|
||||
<Box p={4} height="100%" overflowY="auto">
|
||||
|
@ -183,7 +183,7 @@ export function GroupSwitcher(props: {
|
||||
/>
|
||||
</Link>)}
|
||||
<Link to={navTo("/popover/settings")}>
|
||||
<Icon color='gray' display="block" m={2} icon="Gear" />
|
||||
<Icon color='gray' display="block" m={1} icon="Gear" />
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
|
@ -7,6 +7,7 @@ import {
|
||||
} from "react-router-dom";
|
||||
import { Col, Box, Text } from "@tlon/indigo-react";
|
||||
import _ from "lodash";
|
||||
import Helmet from 'react-helmet';
|
||||
|
||||
import { Resource } from "./Resource";
|
||||
import { PopoverRoutes } from "./PopoverRoutes";
|
||||
@ -68,8 +69,6 @@ export function GroupsPane(props: GroupsPaneProps) {
|
||||
group={group!}
|
||||
api={api}
|
||||
s3={props.s3}
|
||||
hideAvatars={props.hideAvatars}
|
||||
hideNicknames={props.hideNicknames}
|
||||
notificationsGroupConfig={props.notificationsGroupConfig}
|
||||
|
||||
{...routeProps}
|
||||
@ -133,28 +132,36 @@ export function GroupsPane(props: GroupsPaneProps) {
|
||||
const appPath = `/ship/${host}/${name}`;
|
||||
const association = associations.graph[appPath];
|
||||
const resourceUrl = `${baseUrl}/join/${app}${appPath}`;
|
||||
let title = groupAssociation?.metadata?.title ?? 'Landscape';
|
||||
|
||||
if (!association) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
title += ` - ${association.metadata.title}`;
|
||||
return (
|
||||
<Skeleton
|
||||
recentGroups={recentGroups}
|
||||
mobileHide
|
||||
selected={appPath}
|
||||
{...props}
|
||||
baseUrl={baseUrl}
|
||||
>
|
||||
<UnjoinedResource
|
||||
graphKeys={props.graphKeys}
|
||||
notebooks={props.notebooks}
|
||||
inbox={props.inbox}
|
||||
<>
|
||||
<Helmet defer={false}>
|
||||
<title>{props.notificationsCount ? `(${String(props.notificationsCount)}) ` : ''}{ title }</title>
|
||||
</Helmet>
|
||||
<Skeleton
|
||||
recentGroups={recentGroups}
|
||||
mobileHide
|
||||
selected={appPath}
|
||||
{...props}
|
||||
baseUrl={baseUrl}
|
||||
api={api}
|
||||
association={association}
|
||||
/>
|
||||
{popovers(routeProps, resourceUrl)}
|
||||
</Skeleton>
|
||||
>
|
||||
<UnjoinedResource
|
||||
graphKeys={props.graphKeys}
|
||||
notebooks={props.notebooks}
|
||||
inbox={props.inbox}
|
||||
baseUrl={baseUrl}
|
||||
api={api}
|
||||
association={association}
|
||||
/>
|
||||
{popovers(routeProps, resourceUrl)}
|
||||
</Skeleton>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
@ -186,20 +193,26 @@ export function GroupsPane(props: GroupsPaneProps) {
|
||||
const hasDescription = groupAssociation?.metadata?.description;
|
||||
const description = (hasDescription && hasDescription !== "")
|
||||
? hasDescription : "Create or select a channel to get started"
|
||||
const title = groupAssociation?.metadata?.title ?? 'Landscape';
|
||||
return (
|
||||
<Skeleton recentGroups={recentGroups} {...props} baseUrl={baseUrl}>
|
||||
<Col
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
display={["none", "flex"]}
|
||||
p='4'
|
||||
>
|
||||
<Box p="4"><Text fontSize="0" color='gray'>
|
||||
{description}
|
||||
</Text></Box>
|
||||
</Col>
|
||||
{popovers(routeProps, baseUrl)}
|
||||
</Skeleton>
|
||||
<>
|
||||
<Helmet defer={false}>
|
||||
<title>{props.notificationsCount ? `(${String(props.notificationsCount)}) ` : ''}{ title }</title>
|
||||
</Helmet>
|
||||
<Skeleton recentGroups={recentGroups} {...props} baseUrl={baseUrl}>
|
||||
<Col
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
display={["none", "flex"]}
|
||||
p='4'
|
||||
>
|
||||
<Box p="4"><Text fontSize="0" color='gray'>
|
||||
{description}
|
||||
</Text></Box>
|
||||
</Col>
|
||||
{popovers(routeProps, baseUrl)}
|
||||
</Skeleton>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
@ -105,7 +105,7 @@ export function NewGroup(props: NewGroupProps & RouteComponentProps) {
|
||||
<Checkbox
|
||||
id="isPrivate"
|
||||
label="Private Group"
|
||||
caption="Is your group private?"
|
||||
caption="Anyone can join a public group. A private group is only joinable by invite."
|
||||
/>
|
||||
<AsyncButton>Create Group</AsyncButton>
|
||||
</Col>
|
||||
|
@ -31,6 +31,7 @@ import { Dropdown } from '~/views/components/Dropdown';
|
||||
import GlobalApi from '~/logic/api/global';
|
||||
import { StatelessAsyncAction } from '~/views/components/StatelessAsyncAction';
|
||||
import styled from 'styled-components';
|
||||
import useLocalState from '~/logic/state/local';
|
||||
|
||||
const TruncText = styled(Box)`
|
||||
white-space: nowrap;
|
||||
@ -104,10 +105,8 @@ export function Participants(props: {
|
||||
group: Group;
|
||||
association: Association;
|
||||
api: GlobalApi;
|
||||
hideAvatars: boolean;
|
||||
hideNicknames: boolean;
|
||||
}) {
|
||||
const { api, hideAvatars, hideNicknames } = props;
|
||||
const { api } = props;
|
||||
const tabFilters: Record<
|
||||
ParticipantsTabId,
|
||||
(p: Participant) => boolean
|
||||
@ -232,8 +231,6 @@ export function Participants(props: {
|
||||
group={props.group}
|
||||
contact={c}
|
||||
association={props.association}
|
||||
hideAvatars={hideAvatars}
|
||||
hideNicknames={hideNicknames}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
@ -254,11 +251,12 @@ function Participant(props: {
|
||||
group: Group;
|
||||
role?: RoleTags;
|
||||
api: GlobalApi;
|
||||
hideAvatars: boolean;
|
||||
hideNicknames: boolean;
|
||||
}) {
|
||||
const { contact, association, group, api } = props;
|
||||
const { title } = association.metadata;
|
||||
const { hideAvatars, hideNicknames } = useLocalState(
|
||||
({ hideAvatars, hideNicknames }) => ({ hideAvatars, hideNicknames })
|
||||
);
|
||||
|
||||
const color = uxToHex(contact.color);
|
||||
const isInvite = 'invite' in group.policy;
|
||||
@ -296,13 +294,13 @@ function Participant(props: {
|
||||
}, [api, association]);
|
||||
|
||||
const avatar =
|
||||
contact?.avatar !== null && !props.hideAvatars ? (
|
||||
contact?.avatar !== null && !hideAvatars ? (
|
||||
<img src={contact.avatar} height={32} width={32} className="dib" />
|
||||
) : (
|
||||
<Sigil ship={contact.patp} size={32} color={`#${color}`} />
|
||||
);
|
||||
|
||||
const hasNickname = contact.nickname && !props.hideNicknames;
|
||||
const hasNickname = contact.nickname && !hideNicknames;
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -38,8 +38,6 @@ export function PopoverRoutes(
|
||||
association: Association;
|
||||
s3: S3State;
|
||||
api: GlobalApi;
|
||||
hideAvatars: boolean;
|
||||
hideNicknames: boolean;
|
||||
notificationsGroupConfig: GroupNotificationsConfig;
|
||||
rootIdentity: Contact;
|
||||
} & RouteComponentProps
|
||||
@ -135,8 +133,6 @@ export function PopoverRoutes(
|
||||
contacts={props.contacts}
|
||||
association={props.association}
|
||||
api={props.api}
|
||||
hideAvatars={props.hideAvatars}
|
||||
hideNicknames={props.hideNicknames}
|
||||
/>
|
||||
)}
|
||||
{view === "profile" && (
|
||||
@ -144,8 +140,6 @@ export function PopoverRoutes(
|
||||
contact={props.contacts[window.ship]}
|
||||
rootIdentity={props.rootIdentity}
|
||||
api={props.api}
|
||||
hideAvatars={props.hideAvatars}
|
||||
hideNicknames={props.hideNicknames}
|
||||
path={props.association["group-path"]}
|
||||
s3={props.s3}
|
||||
/>
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React, { useCallback } from "react";
|
||||
import { Row, Box, Col } from "@tlon/indigo-react";
|
||||
import styled from "styled-components";
|
||||
import { Link } from "react-router-dom";
|
||||
import Helmet from 'react-helmet';
|
||||
|
||||
import { ChatResource } from "~/views/apps/chat/ChatResource";
|
||||
import { PublishResource } from "~/views/apps/publish/PublishResource";
|
||||
@ -34,47 +34,58 @@ export function Resource(props: ResourceProps) {
|
||||
const relativePath = (p: string) =>
|
||||
`${props.baseUrl}/resource/${app}${appPath}${p}`;
|
||||
const skelProps = { api, association };
|
||||
let title = props.association.metadata.title;
|
||||
if ('workspace' in props) {
|
||||
if ('group' in props.workspace && props.workspace.group in props.associations.contacts) {
|
||||
title = `${props.associations.contacts[props.workspace.group].metadata.title} - ${props.association.metadata.title}`;
|
||||
}
|
||||
}
|
||||
return (
|
||||
<Switch>
|
||||
<Route
|
||||
path={relativePath("/settings")}
|
||||
render={(routeProps) => {
|
||||
return (
|
||||
<>
|
||||
<Helmet defer={false}>
|
||||
<title>{props.notificationsCount ? `(${String(props.notificationsCount)}) ` : ''}{ title }</title>
|
||||
</Helmet>
|
||||
<Switch>
|
||||
<Route
|
||||
path={relativePath("/settings")}
|
||||
render={(routeProps) => {
|
||||
return (
|
||||
<ResourceSkeleton
|
||||
baseUrl={props.baseUrl}
|
||||
{...skelProps}
|
||||
>
|
||||
<ChannelSettings
|
||||
groups={props.groups}
|
||||
contacts={props.contacts}
|
||||
associations={props.associations}
|
||||
api={api}
|
||||
association={association}
|
||||
/>
|
||||
</ResourceSkeleton>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<Route
|
||||
path={relativePath("")}
|
||||
render={(routeProps) => (
|
||||
<ResourceSkeleton
|
||||
notificationsGraphConfig={props.notificationsGraphConfig}
|
||||
notificationsChatConfig={props.notificationsChatConfig}
|
||||
baseUrl={props.baseUrl}
|
||||
{...skelProps}
|
||||
atRoot
|
||||
>
|
||||
<ChannelSettings
|
||||
groups={props.groups}
|
||||
contacts={props.contacts}
|
||||
associations={props.associations}
|
||||
api={api}
|
||||
association={association}
|
||||
/>
|
||||
{app === "chat" ? (
|
||||
<ChatResource {...props} />
|
||||
) : app === "publish" ? (
|
||||
<PublishResource {...props} />
|
||||
) : (
|
||||
<LinkResource {...props} />
|
||||
)}
|
||||
</ResourceSkeleton>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<Route
|
||||
path={relativePath("")}
|
||||
render={(routeProps) => (
|
||||
<ResourceSkeleton
|
||||
notificationsGraphConfig={props.notificationsGraphConfig}
|
||||
notificationsChatConfig={props.notificationsChatConfig}
|
||||
baseUrl={props.baseUrl}
|
||||
{...skelProps}
|
||||
atRoot
|
||||
>
|
||||
{app === "chat" ? (
|
||||
<ChatResource {...props} />
|
||||
) : app === "publish" ? (
|
||||
<PublishResource {...props} />
|
||||
) : (
|
||||
<LinkResource {...props} />
|
||||
)}
|
||||
</ResourceSkeleton>
|
||||
)}
|
||||
/>
|
||||
</Switch>
|
||||
)}
|
||||
/>
|
||||
</Switch>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -38,12 +38,6 @@ export function ResourceSkeleton(props: ResourceSkeletonProps) {
|
||||
const workspace =
|
||||
baseUrl === "/~landscape/home" ? "/home" : association["group-path"];
|
||||
const title = props.title || association?.metadata?.title;
|
||||
const disableRemoteContent = {
|
||||
audioShown: false,
|
||||
imageShown: false,
|
||||
oembedShown: false,
|
||||
videoShown: false,
|
||||
};
|
||||
return (
|
||||
<Col width="100%" height="100%" overflowY="hidden">
|
||||
<Box
|
||||
@ -91,9 +85,9 @@ export function ResourceSkeleton(props: ResourceSkeletonProps) {
|
||||
>
|
||||
<RichText
|
||||
color="gray"
|
||||
remoteContentPolicy={disableRemoteContent}
|
||||
mb="0"
|
||||
display="inline-block"
|
||||
disableRemoteContent
|
||||
>
|
||||
{association?.metadata?.description}
|
||||
</RichText>
|
||||
|
@ -46,16 +46,17 @@ export function useGraphModule(
|
||||
): SidebarAppConfig {
|
||||
const getStatus = useCallback(
|
||||
(s: string) => {
|
||||
const unreads = graphUnreads?.[s]?.['/']?.unreads;
|
||||
if(typeof unreads === 'number' ? unreads > 0 : unreads?.size ?? 0 > 0) {
|
||||
return 'unread';
|
||||
}
|
||||
const [, , host, name] = s.split("/");
|
||||
const graphKey = `${host.slice(1)}/${name}`;
|
||||
|
||||
if (!graphKeys.has(graphKey)) {
|
||||
return "unsubscribed";
|
||||
}
|
||||
|
||||
const unreads = graphUnreads?.[s]?.['/']?.unreads;
|
||||
if (typeof unreads === 'number' ? unreads > 0 : unreads?.size ?? 0 > 0) {
|
||||
return 'unread';
|
||||
}
|
||||
|
||||
return undefined;
|
||||
},
|
||||
[graphs, graphKeys, graphUnreads]
|
||||
|
@ -27,7 +27,7 @@ function SidebarItemIndicator(props: { status?: SidebarItemStatus }) {
|
||||
const getAppIcon = (app: string, mod: string) => {
|
||||
if (app === "graph") {
|
||||
if (mod === "link") {
|
||||
return "Links";
|
||||
return "Collection";
|
||||
}
|
||||
return _.capitalize(mod);
|
||||
}
|
||||
@ -93,7 +93,7 @@ export function SidebarItem(props: {
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
py={1}
|
||||
pl={4}
|
||||
pl={2}
|
||||
pr={2}
|
||||
selected={selected}
|
||||
>
|
||||
|
@ -43,10 +43,10 @@ export function SidebarListHeader(props: {
|
||||
justifyContent="space-between"
|
||||
py={2}
|
||||
pr={2}
|
||||
pl={3}
|
||||
pl={2}
|
||||
>
|
||||
<Box flexShrink='0'>
|
||||
<Text>
|
||||
<Text bold>
|
||||
{props.initialValues.hideUnjoined ? "Joined Channels" : "All Channels"}
|
||||
</Text>
|
||||
</Box>
|
||||
|
@ -1,5 +1,6 @@
|
||||
import React, { Component, useEffect, useCallback } from 'react';
|
||||
import { Route, Switch, RouteComponentProps } from 'react-router-dom';
|
||||
import Helmet from 'react-helmet';
|
||||
|
||||
import './css/custom.css';
|
||||
|
||||
@ -67,8 +68,6 @@ export function DMRedirect(props: LandscapeProps & RouteComponentProps & { ship:
|
||||
|
||||
export default class Landscape extends Component<LandscapeProps, {}> {
|
||||
componentDidMount() {
|
||||
document.title = 'OS1 - Landscape';
|
||||
|
||||
this.props.subscription.startApp('groups');
|
||||
this.props.subscription.startApp('graph');
|
||||
}
|
||||
@ -78,71 +77,76 @@ export default class Landscape extends Component<LandscapeProps, {}> {
|
||||
const { api } = props;
|
||||
|
||||
return (
|
||||
<Switch>
|
||||
<Route path="/~landscape/ship/:host/:name"
|
||||
render={routeProps => {
|
||||
const {
|
||||
host,
|
||||
name
|
||||
} = routeProps.match.params as Record<string, string>;
|
||||
const groupPath = `/ship/${host}/${name}`;
|
||||
const baseUrl = `/~landscape${groupPath}`;
|
||||
const ws: Workspace = { type: 'group', group: groupPath };
|
||||
<>
|
||||
<Helmet defer={false}>
|
||||
<title>{ props.notificationsCount ? `(${String(props.notificationsCount) }) `: '' }Landscape</title>
|
||||
</Helmet>
|
||||
<Switch>
|
||||
<Route path="/~landscape/ship/:host/:name"
|
||||
render={routeProps => {
|
||||
const {
|
||||
host,
|
||||
name
|
||||
} = routeProps.match.params as Record<string, string>;
|
||||
const groupPath = `/ship/${host}/${name}`;
|
||||
const baseUrl = `/~landscape${groupPath}`;
|
||||
const ws: Workspace = { type: 'group', group: groupPath };
|
||||
|
||||
return (
|
||||
<GroupsPane workspace={ws} baseUrl={baseUrl} {...props} />
|
||||
)
|
||||
}}/>
|
||||
<Route path="/~landscape/home"
|
||||
return (
|
||||
<GroupsPane workspace={ws} baseUrl={baseUrl} {...props} />
|
||||
)
|
||||
}}/>
|
||||
<Route path="/~landscape/home"
|
||||
render={routeProps => {
|
||||
const ws: Workspace = { type: 'home' };
|
||||
return (
|
||||
<GroupsPane workspace={ws} baseUrl="/~landscape/home" {...props} />
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<Route path="/~landscape/new"
|
||||
render={routeProps=> {
|
||||
return (
|
||||
<Body>
|
||||
<Box maxWidth="300px">
|
||||
<NewGroup
|
||||
groups={props.groups}
|
||||
contacts={props.contacts}
|
||||
api={props.api}
|
||||
{...routeProps}
|
||||
/>
|
||||
</Box>
|
||||
</Body>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<Route path='/~landscape/dm/:ship?'
|
||||
render={routeProps => {
|
||||
const ws: Workspace = { type: 'home' };
|
||||
return (
|
||||
<GroupsPane workspace={ws} baseUrl="/~landscape/home" {...props} />
|
||||
);
|
||||
const { ship } = routeProps.match.params;
|
||||
return <DMRedirect {...routeProps} {...props} ship={ship} />
|
||||
}}
|
||||
/>
|
||||
<Route path="/~landscape/new"
|
||||
render={routeProps=> {
|
||||
return (
|
||||
<Body>
|
||||
<Box maxWidth="300px">
|
||||
<NewGroup
|
||||
groups={props.groups}
|
||||
contacts={props.contacts}
|
||||
api={props.api}
|
||||
{...routeProps}
|
||||
/>
|
||||
</Box>
|
||||
</Body>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<Route path='/~landscape/dm/:ship?'
|
||||
render={routeProps => {
|
||||
const { ship } = routeProps.match.params;
|
||||
return <DMRedirect {...routeProps} {...props} ship={ship} />
|
||||
}}
|
||||
/>
|
||||
<Route path="/~landscape/join/:ship?/:name?"
|
||||
render={routeProps=> {
|
||||
const { ship, name } = routeProps.match.params;
|
||||
const autojoin = ship && name ? `${ship}/${name}` : null;
|
||||
return (
|
||||
<Body>
|
||||
<Box maxWidth="300px">
|
||||
<JoinGroup
|
||||
groups={props.groups}
|
||||
contacts={props.contacts}
|
||||
api={props.api}
|
||||
autojoin={autojoin}
|
||||
{...routeProps}
|
||||
/>
|
||||
</Box>
|
||||
</Body>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</Switch>
|
||||
/>
|
||||
<Route path="/~landscape/join/:ship?/:name?"
|
||||
render={routeProps=> {
|
||||
const { ship, name } = routeProps.match.params;
|
||||
const autojoin = ship && name ? `${ship}/${name}` : null;
|
||||
return (
|
||||
<Body>
|
||||
<Box maxWidth="300px">
|
||||
<JoinGroup
|
||||
groups={props.groups}
|
||||
contacts={props.contacts}
|
||||
api={props.api}
|
||||
autojoin={autojoin}
|
||||
{...routeProps}
|
||||
/>
|
||||
</Box>
|
||||
</Body>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</Switch>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user