interface: cleaning up imports

This commit is contained in:
Tyler Brown Cifu Shuster 2021-02-14 14:11:27 -08:00
parent cf2380bb6c
commit 1b19a95fea
191 changed files with 2245 additions and 3029 deletions

View File

@ -1,6 +1,5 @@
import _ from "lodash";
import { uuid } from "../lib/util";
import { Patp, Path } from "~/types/noun";
import _ from 'lodash';
import { Patp, Path } from '@urbit/api';
import BaseStore from '../store/base';
export default class BaseApi<S extends object = {}> {
@ -26,8 +25,8 @@ export default class BaseApi<S extends object = {}> {
data: event,
from: {
ship,
path,
},
path
}
});
},
(qui) => {
@ -50,8 +49,12 @@ export default class BaseApi<S extends object = {}> {
appl,
mark,
data,
(json) => { resolve(json); },
(err) => { reject(err); }
(json) => {
resolve(json);
},
(err) => {
reject(err);
}
);
});
}
@ -69,5 +72,4 @@ export default class BaseApi<S extends object = {}> {
return res.json();
}
}

View File

@ -1,8 +1,7 @@
import BaseApi from './base';
import { StoreState } from '../store/type';
import { Patp, Path, Enc } from '~/types/noun';
import { Contact, ContactEdit } from '~/types/contact-update';
import { GroupPolicy, Resource } from '~/types/group-update';
import { Patp } from '@urbit/api';
import { ContactEdit } from '@urbit/api/contacts';
export default class ContactsApi extends BaseApi<StoreState> {
add(ship: Patp, contact: any) {
@ -31,7 +30,7 @@ export default class ContactsApi extends BaseApi<StoreState> {
ship,
'edit-field': editField,
timestamp: Date.now()
},
}
});
}
@ -62,7 +61,7 @@ export default class ContactsApi extends BaseApi<StoreState> {
return this.action(
'contact-push-hook',
'contact-share',
{ share: recipient },
{ share: recipient }
);
}
@ -85,7 +84,7 @@ export default class ContactsApi extends BaseApi<StoreState> {
}
private storeAction(action: any): Promise<any> {
return this.action('contact-store', 'contact-update', action)
return this.action('contact-store', 'contact-update', action);
}
private viewAction(threadName: string, action: any) {

View File

@ -1,4 +1,4 @@
import { Patp } from '~/types/noun';
import { Patp } from '@urbit/api';
import BaseApi from './base';
import { StoreState } from '../store/type';
import GlobalStore from '../store/store';
@ -10,7 +10,7 @@ import GroupsApi from './groups';
import LaunchApi from './launch';
import GraphApi from './graph';
import S3Api from './s3';
import {HarkApi} from './hark';
import { HarkApi } from './hark';
import SettingsApi from './settings';
export default class GlobalApi extends BaseApi<StoreState> {

View File

@ -1,14 +1,14 @@
import BaseApi from './base';
import { StoreState } from '../store/type';
import { Patp, Path, PatpNoSig } from '~/types/noun';
import { Patp, Path } from '@urbit/api';
import _ from 'lodash';
import {makeResource, resourceFromPath} from '../lib/group';
import {GroupPolicy, Enc, Post, NodeMap, Content, Resource} from '~/types';
import { makeResource, resourceFromPath } from '../lib/group';
import { GroupPolicy, Enc, Post, Content } from '@urbit/api';
import { numToUd, unixToDa, decToUd, deSig, resourceAsPath } from '~/logic/lib/util';
export const createBlankNodeWithChildPost = (
parentIndex: string = '',
childIndex: string = '',
parentIndex = '',
childIndex = '',
contents: Content[]
) => {
const date = unixToDa(Date.now()).toString();
@ -37,11 +37,11 @@ export const createBlankNodeWithChildPost = (
signatures: []
},
children: childGraph
};
};
};
function markPending(nodes: any) {
_.forEach(nodes, node => {
_.forEach(nodes, (node) => {
node.post.author = deSig(node.post.author);
node.post.pending = true;
markPending(node.children || {});
@ -50,8 +50,8 @@ function markPending(nodes: any) {
export const createPost = (
contents: Content[],
parentIndex: string = '',
childIndex:string = 'DATE_PLACEHOLDER'
parentIndex = '',
childIndex = 'DATE_PLACEHOLDER'
) => {
if (childIndex === 'DATE_PLACEHOLDER') {
childIndex = unixToDa(Date.now()).toString();
@ -80,11 +80,10 @@ function moduleToMark(mod: string): string | undefined {
}
export default class GraphApi extends BaseApi<StoreState> {
joiningGraphs = new Set<string>();
private storeAction(action: any): Promise<any> {
return this.action('graph-store', 'graph-update', action)
return this.action('graph-store', 'graph-update', action);
}
private viewAction(threadName: string, action: any) {
@ -106,12 +105,12 @@ export default class GraphApi extends BaseApi<StoreState> {
const resource = makeResource(`~${window.ship}`, name);
return this.viewAction('graph-create', {
"create": {
'create': {
resource,
title,
description,
associated,
"module": mod,
'module': mod,
mark: moduleToMark(mod)
}
});
@ -127,12 +126,12 @@ export default class GraphApi extends BaseApi<StoreState> {
const resource = makeResource(`~${window.ship}`, name);
return this.viewAction('graph-create', {
"create": {
'create': {
resource,
title,
description,
associated: { policy },
"module": mod,
'module': mod,
mark: moduleToMark(mod)
}
});
@ -148,9 +147,9 @@ export default class GraphApi extends BaseApi<StoreState> {
return this.viewAction('graph-join', {
join: {
resource,
ship,
ship
}
}).then(res => {
}).then((res) => {
this.joiningGraphs.delete(rid);
return res;
});
@ -159,7 +158,7 @@ export default class GraphApi extends BaseApi<StoreState> {
deleteGraph(name: string) {
const resource = makeResource(`~${window.ship}`, name);
return this.viewAction('graph-delete', {
"delete": {
'delete': {
resource
}
});
@ -168,7 +167,7 @@ export default class GraphApi extends BaseApi<StoreState> {
leaveGraph(ship: Patp, name: string) {
const resource = makeResource(ship, name);
return this.viewAction('graph-leave', {
"leave": {
'leave': {
resource
}
});
@ -203,7 +202,7 @@ export default class GraphApi extends BaseApi<StoreState> {
}
addPost(ship: Patp, name: string, post: Post) {
let nodes = {};
const nodes = {};
nodes[post.index] = {
post,
children: null
@ -212,7 +211,7 @@ export default class GraphApi extends BaseApi<StoreState> {
}
addNode(ship: Patp, name: string, node: Object) {
let nodes = {};
const nodes = {};
nodes[node.post.index] = node;
return this.addNodes(ship, name, nodes);
@ -300,7 +299,6 @@ export default class GraphApi extends BaseApi<StoreState> {
this.store.handleEvent({ data });
}
getGraphSubset(ship: string, resource: string, start: string, end: string) {
return this.scry<any>(
'graph-store',

View File

@ -1,14 +1,14 @@
import BaseApi from './base';
import { StoreState } from '../store/type';
import { Path, Patp, Enc } from '~/types/noun';
import { Path, Patp, Enc } from '@urbit/api';
import {
GroupAction,
GroupPolicy,
Resource,
Tag,
GroupPolicyDiff,
} from '~/types/group-update';
import {makeResource} from '../lib/group';
GroupPolicyDiff
} from '@urbit/api/groups';
import { makeResource } from '../lib/group';
export default class GroupsApi extends BaseApi<StoreState> {
remove(resource: Resource, ships: Patp[]) {
@ -38,7 +38,7 @@ export default class GroupsApi extends BaseApi<StoreState> {
join(ship: string, name: string) {
const resource = makeResource(ship, name);
return this.viewAction({ join: { resource, ship }});
return this.viewAction({ join: { resource, ship } });
}
create(name: string, policy: Enc<GroupPolicy>, title: string, description: string) {
@ -76,7 +76,6 @@ export default class GroupsApi extends BaseApi<StoreState> {
description
}
});
}
private proxyAction(action: GroupAction) {
@ -93,6 +92,5 @@ export default class GroupsApi extends BaseApi<StoreState> {
private viewAction(action: any) {
return this.action('group-view', 'group-view-action', action);
}
}

View File

@ -1,24 +1,23 @@
import BaseApi from "./base";
import { StoreState } from "../store/type";
import { dateToDa, decToUd } from "../lib/util";
import {NotifIndex, IndexedNotification, Association, GraphNotifDescription} from "~/types";
import BaseApi from './base';
import { StoreState } from '../store/type';
import { dateToDa, decToUd } from '../lib/util';
import { NotifIndex, IndexedNotification, Association, GraphNotifDescription } from '@urbit/api';
import { BigInteger } from 'big-integer';
import {getParentIndex} from "../lib/notification";
import { getParentIndex } from '../lib/notification';
export class HarkApi extends BaseApi<StoreState> {
private harkAction(action: any): Promise<any> {
return this.action("hark-store", "hark-action", action);
return this.action('hark-store', 'hark-action', action);
}
private graphHookAction(action: any) {
return this.action("hark-graph-hook", "hark-graph-hook-action", action);
return this.action('hark-graph-hook', 'hark-graph-hook-action', action);
}
private groupHookAction(action: any) {
return this.action("hark-group-hook", "hark-group-hook-action", action);
return this.action('hark-group-hook', 'hark-group-hook-action', action);
}
private actOnNotification(frond: string, intTime: BigInteger, index: NotifIndex) {
const time = decToUd(intTime.toString());
return this.harkAction({
@ -74,12 +73,10 @@ export class HarkApi extends BaseApi<StoreState> {
module: association.metadata.module,
description,
index: parent
} },
} }
});
}
markEachAsRead(association: Association, parent: string, child: string, description: GraphNotifDescription, mod: string) {
return this.harkAction({
'read-each': {
@ -116,7 +113,7 @@ export class HarkApi extends BaseApi<StoreState> {
mute(notif: IndexedNotification) {
if('graph' in notif.index && 'graph' in notif.notification.contents) {
const { index } = notif;
const parentIndex = getParentIndex(index.graph, notif.notification.contents.graph)
const parentIndex = getParentIndex(index.graph, notif.notification.contents.graph);
if(!parentIndex) {
return Promise.resolve();
}
@ -132,7 +129,7 @@ export class HarkApi extends BaseApi<StoreState> {
unmute(notif: IndexedNotification) {
if('graph' in notif.index && 'graph' in notif.notification.contents) {
const { index } = notif;
const parentIndex = getParentIndex(index.graph, notif.notification.contents.graph)
const parentIndex = getParentIndex(index.graph, notif.notification.contents.graph);
if(!parentIndex) {
return Promise.resolve();
}
@ -147,7 +144,7 @@ export class HarkApi extends BaseApi<StoreState> {
ignoreGroup(group: string) {
return this.groupHookAction({
ignore: group
})
});
}
ignoreGraph(graph: string, index: string) {
@ -156,13 +153,13 @@ export class HarkApi extends BaseApi<StoreState> {
graph,
index
}
})
});
}
listenGroup(group: string) {
return this.groupHookAction({
listen: group
})
});
}
listenGraph(graph: string, index: string) {
@ -171,7 +168,7 @@ export class HarkApi extends BaseApi<StoreState> {
graph,
index
}
})
});
}
async getMore(): Promise<boolean> {
@ -183,16 +180,16 @@ export class HarkApi extends BaseApi<StoreState> {
async getSubset(offset:number, count:number, isArchive: boolean) {
const where = isArchive ? 'archive' : 'inbox';
const data = await this.scry("hark-store", `/recent/${where}/${offset}/${count}`);
const data = await this.scry('hark-store', `/recent/${where}/${offset}/${count}`);
this.store.handleEvent({ data });
}
async getTimeSubset(start?: Date, end?: Date) {
const s = start ? dateToDa(start) : "-";
const e = end ? dateToDa(end) : "-";
const result = await this.scry("hark-hook", `/recent/${s}/${e}`);
const s = start ? dateToDa(start) : '-';
const e = end ? dateToDa(end) : '-';
const result = await this.scry('hark-hook', `/recent/${s}/${e}`);
this.store.handleEvent({
data: result,
data: result
});
}
}

View File

@ -1,6 +1,6 @@
import BaseApi from "./base";
import { StoreState } from "../store/type";
import { Serial, Path } from "~/types/noun";
import BaseApi from './base';
import { StoreState } from '../store/type';
import { Serial, Path } from '@urbit/api';
export default class InviteApi extends BaseApi<StoreState> {
accept(app: string, uid: Serial) {

View File

@ -2,7 +2,7 @@ import BaseApi from './base';
import { StoreState } from '../store/type';
export default class LaunchApi extends BaseApi<StoreState> {
add(name: string, tile = { basic : { title: '', linkedUrl: '', iconUrl: '' }}) {
add(name: string, tile = { basic : { title: '', linkedUrl: '', iconUrl: '' } }) {
return this.launchAction({ add: { name, tile } });
}
@ -15,7 +15,7 @@ export default class LaunchApi extends BaseApi<StoreState> {
}
changeIsShown(name: string, isShown = true) {
return this.launchAction({ 'change-is-shown': { name, isShown }});
return this.launchAction({ 'change-is-shown': { name, isShown } });
}
weather(location: string) {

View File

@ -1,9 +1,9 @@
import BaseApi from "./base";
import { StoreState } from "../store/type";
import BaseApi from './base';
import { StoreState } from '../store/type';
export default class LocalApi extends BaseApi<StoreState> {
getBaseHash() {
this.scry<string>('file-server', '/clay/base/hash').then(baseHash => {
this.scry<string>('file-server', '/clay/base/hash').then((baseHash) => {
this.store.handleEvent({ data: { local: { baseHash } } });
});
}
@ -11,5 +11,4 @@ export default class LocalApi extends BaseApi<StoreState> {
dehydrate() {
this.store.dehydrate();
}
}

View File

@ -1,12 +1,10 @@
import BaseApi from './base';
import { StoreState } from '../store/type';
import { Path, Patp, Association, Metadata, MetadataUpdatePreview } from '~/types';
import {uxToHex} from '../lib/util';
import { Path, Patp, Association, Metadata, MetadataUpdatePreview } from '@urbit/api';
import { uxToHex } from '../lib/util';
export default class MetadataApi extends BaseApi<StoreState> {
metadataAdd(appName: string, resource: Path, group: Path, title: string, description: string, dateCreated: string, color: string, moduleName: string) {
const creator = `~${this.ship}`;
return this.metadataAction({
@ -44,9 +42,9 @@ export default class MetadataApi extends BaseApi<StoreState> {
}
update(association: Association, newMetadata: Partial<Metadata>) {
const metadata = {...association.metadata, ...newMetadata };
const metadata = { ...association.metadata, ...newMetadata };
metadata.color = uxToHex(metadata.color);
return this.metadataAction({
return this.metadataAction({
add: {
group: association.group,
resource: {
@ -69,10 +67,10 @@ export default class MetadataApi extends BaseApi<StoreState> {
}
done = true;
tempChannel.delete();
reject(new Error("offline"))
reject(new Error('offline'));
}, 15000);
tempChannel.subscribe(window.ship, "metadata-pull-hook", `/preview${group}`,
tempChannel.subscribe(window.ship, 'metadata-pull-hook', `/preview${group}`,
(err) => {
console.error(err);
reject(err);
@ -88,24 +86,22 @@ export default class MetadataApi extends BaseApi<StoreState> {
} else {
done = true;
tempChannel.delete();
reject(new Error("no-permissions"));
reject(new Error('no-permissions'));
}
},
(quit) => {
tempChannel.delete();
if(!done) {
reject(new Error("offline"))
reject(new Error('offline'));
}
},
(a) => {
console.log(a);
}
);
})
});
}
private metadataAction(data) {
return this.action('metadata-push-hook', 'metadata-update', data);
}

View File

@ -1,10 +1,8 @@
import BaseApi from './base';
import { StoreState } from '../store/type';
import {S3Update} from '../../types/s3-update';
import { S3Update } from '../../types/s3-update';
export default class S3Api extends BaseApi<StoreState> {
setCurrentBucket(bucket: string) {
return this.s3Action({ 'set-current-bucket': bucket });
}
@ -32,6 +30,5 @@ export default class S3Api extends BaseApi<StoreState> {
private s3Action(data: any) {
return this.action('s3-store', 's3-action', data);
}
}

View File

@ -1,13 +1,9 @@
import BaseApi from './base';
import { StoreState } from '../store/type';
import {
SettingsUpdate,
SettingsData,
Key,
import { Key,
Value,
Bucket,
} from '~/types/settings';
Bucket
} from '@urbit/api/settings';
export default class SettingsApi extends BaseApi<StoreState> {
private storeAction(action: SettingsEvent): Promise<any> {
@ -16,59 +12,59 @@ export default class SettingsApi extends BaseApi<StoreState> {
putBucket(key: Key, bucket: Bucket) {
this.storeAction({
"put-bucket": {
"bucket-key": key,
"bucket": bucket,
'put-bucket': {
'bucket-key': key,
'bucket': bucket
}
});
}
delBucket(key: Key) {
this.storeAction({
"del-bucket": {
"bucket-key": key,
'del-bucket': {
'bucket-key': key
}
});
}
putEntry(buc: Key, key: Key, val: Value) {
return this.storeAction({
"put-entry": {
"bucket-key": buc,
"entry-key": key,
"value": val,
'put-entry': {
'bucket-key': buc,
'entry-key': key,
'value': val
}
});
}
delEntry(buc: Key, key: Key) {
this.storeAction({
"put-entry": {
"bucket-key": buc,
"entry-key": key,
'put-entry': {
'bucket-key': buc,
'entry-key': key
}
});
}
async getAll() {
const data = await this.scry("settings-store", "/all");
this.store.handleEvent({data: {"settings-data": data.all}});
const data = await this.scry('settings-store', '/all');
this.store.handleEvent({ data: { 'settings-data': data.all } });
}
async getBucket(bucket: Key) {
const data = await this.scry('settings-store', `/bucket/${bucket}`);
this.store.handleEvent({data: {"settings-data": {
"bucket-key": bucket,
"bucket": data.bucket,
}}});
this.store.handleEvent({ data: { 'settings-data': {
'bucket-key': bucket,
'bucket': data.bucket
} } });
}
async getEntry(bucket: Key, entry: Key) {
const data = await this.scry('settings-store', `/entry/${bucket}/${entry}`);
this.store.handleEvent({data: {"settings-data": {
"bucket-key": bucket,
"entry-key": entry,
"entry": data.entry,
}}});
this.store.handleEvent({ data: { 'settings-data': {
'bucket-key': bucket,
'entry-key': entry,
'entry': data.entry
} } });
}
}

View File

@ -1,4 +1,4 @@
import bigInt, { BigInteger } from "big-integer";
import bigInt, { BigInteger } from 'big-integer';
interface NonemptyNode<V> {
n: [BigInteger, V];
@ -14,7 +14,7 @@ type MapNode<V> = NonemptyNode<V> | null;
*/
export class BigIntOrderedMap<V> implements Iterable<[BigInteger, V]> {
private root: MapNode<V> = null;
size: number = 0;
size = 0;
constructor(initial: [BigInteger, V][] = []) {
initial.forEach(([key, val]) => {
@ -48,13 +48,12 @@ export class BigIntOrderedMap<V> implements Iterable<[BigInteger, V]> {
* Put an item by a key
*/
set(key: BigInteger, value: V): void {
const inner = (node: MapNode<V>) => {
if (!node) {
return {
n: [key, value],
l: null,
r: null,
r: null
};
}
const [k] = node.n;
@ -62,22 +61,22 @@ export class BigIntOrderedMap<V> implements Iterable<[BigInteger, V]> {
this.size--;
return {
...node,
n: [k, value],
n: [k, value]
};
}
if (key.gt(k)) {
const l = inner(node.l);
if (!l) {
throw new Error("invariant violation");
throw new Error('invariant violation');
}
return {
...node,
l,
l
};
}
const r = inner(node.r);
if (!r) {
throw new Error("invariant violation");
throw new Error('invariant violation');
}
return { ...node, r };
@ -133,8 +132,8 @@ export class BigIntOrderedMap<V> implements Iterable<[BigInteger, V]> {
bool,
{
...node,
l,
},
l
}
];
}
@ -143,8 +142,8 @@ export class BigIntOrderedMap<V> implements Iterable<[BigInteger, V]> {
bool,
{
...node,
r,
},
r
}
];
};
const [ret, newRoot] = inner(this.root);
@ -165,12 +164,12 @@ export class BigIntOrderedMap<V> implements Iterable<[BigInteger, V]> {
}
return {
...node.l,
r: inner(node.r),
r: inner(node.r)
};
};
return inner(nod);
}
peekLargest(): [BigInteger, V] | undefined {
const inner = (node: MapNode<V>) => {
if(!node) {
@ -180,7 +179,7 @@ export class BigIntOrderedMap<V> implements Iterable<[BigInteger, V]> {
return inner(node.l);
}
return node.n;
}
};
return inner(this.root);
}
@ -193,7 +192,7 @@ export class BigIntOrderedMap<V> implements Iterable<[BigInteger, V]> {
return inner(node.r);
}
return node.n;
}
};
return inner(this.root);
}
@ -208,7 +207,7 @@ export class BigIntOrderedMap<V> implements Iterable<[BigInteger, V]> {
}
[Symbol.iterator](): IterableIterator<[BigInteger, V]> {
let result: [BigInteger, V][] = [];
const result: [BigInteger, V][] = [];
const inner = (node: MapNode<V>) => {
if (!node) {
return;
@ -227,7 +226,7 @@ export class BigIntOrderedMap<V> implements Iterable<[BigInteger, V]> {
return { value: result[idx++], done: false };
}
return { done: true, value: null };
},
}
};
}
}

View File

@ -1,7 +1,6 @@
export class OrderedMap<V> extends Map<number, V>
implements Iterable<[number, V]> {
[Symbol.iterator](): IterableIterator<[number, V]> {
const sorted = Array.from(super[Symbol.iterator]()).sort(
([a], [b]) => b - a
@ -15,7 +14,7 @@ export class OrderedMap<V> extends Map<number, V>
} else {
return { done: true, value: null };
}
},
}
};
}
}

View File

@ -1,4 +1,4 @@
import bigInt, { BigInteger } from "big-integer";
import bigInt, { BigInteger } from 'big-integer';
export function max(a: BigInteger, b: BigInteger) {
return a.gt(b) ? a : b;

View File

@ -1,7 +1,7 @@
import _ from "lodash";
import { roleTags, RoleTags, Group, Resource } from "~/types/group-update";
import { PatpNoSig, Path } from "~/types/noun";
import {deSig} from "./util";
import _ from 'lodash';
import { roleTags, RoleTags, Group, Resource } from '@urbit/api/groups';
import { PatpNoSig, Path } from '@urbit/api';
import { deSig } from './util';
export function roleForShip(
group: Group,
@ -14,7 +14,7 @@ export function roleForShip(
}
export function resourceFromPath(path: Path): Resource {
const [, , ship, name] = path.split("/");
const [, , ship, name] = path.split('/');
return { ship, name };
}
@ -25,7 +25,7 @@ export function makeResource(ship: string, name: string) {
export function isWriter(group: Group, resource: string) {
const writers: Set<string> | undefined = _.get(
group,
["tags", "graph", resource, "writers"],
['tags', 'graph', resource, 'writers'],
undefined
);
const admins = group?.tags?.role?.admin ?? new Set();
@ -36,18 +36,18 @@ export function isWriter(group: Group, resource: string) {
}
}
export function isChannelAdmin(group: Group, resource: string, ship: string = `~${window.ship}`) {
export function isChannelAdmin(group: Group, resource: string, ship = `~${window.ship}`) {
const role = roleForShip(group, ship.slice(1));
return (
isHost(resource, ship) ||
role === "admin" ||
role === "moderator"
role === 'admin' ||
role === 'moderator'
);
}
export function isHost(resource: string, ship: string = `~${window.ship}`) {
const [, , host] = resource.split("/");
export function isHost(resource: string, ship = `~${window.ship}`) {
const [, , host] = resource.split('/');
return ship === host;
}

View File

@ -1,6 +1,6 @@
import bigInt, { BigInteger } from "big-integer";
import f from "lodash/fp";
import { Unreads } from "~/types";
import bigInt, { BigInteger } from 'big-integer';
import f from 'lodash/fp';
import { Unreads } from '@urbit/api';
export function getLastSeen(
unreads: Unreads,
@ -8,10 +8,10 @@ export function getLastSeen(
index: string
): BigInteger | undefined {
const lastSeenIdx = unreads.graph?.[path]?.[index]?.unreads;
if (!(typeof lastSeenIdx === "string")) {
if (!(typeof lastSeenIdx === 'string')) {
return bigInt.zero;
}
return f.flow(f.split("/"), f.last, (x) => (!!x ? bigInt(x) : undefined))(
return f.flow(f.split('/'), f.last, x => (x ? bigInt(x) : undefined))(
lastSeenIdx
);
}

View File

@ -1,19 +1,19 @@
import { GraphNotifIndex, GraphNotificationContents } from "~/types";
import { GraphNotifIndex, GraphNotificationContents } from '@urbit/api';
export function getParentIndex(
idx: GraphNotifIndex,
contents: GraphNotificationContents
) {
const origIndex = contents[0].index.slice(1).split("/");
const ret = (i: string[]) => `/${i.join("/")}`;
const origIndex = contents[0].index.slice(1).split('/');
const ret = (i: string[]) => `/${i.join('/')}`;
switch (idx.description) {
case "link":
return "/";
case "comment":
case 'link':
return '/';
case 'comment':
return ret(origIndex.slice(0, 1));
case "note":
return "/";
case "mention":
case 'note':
return '/';
case 'mention':
return undefined;
default:
return undefined;

View File

@ -1,4 +1,4 @@
import { Post, GraphNode } from "~/types";
import { Post, GraphNode } from '@urbit/api';
export const buntPost = (): Post => ({
author: '',
@ -10,7 +10,7 @@ export const buntPost = (): Post => ({
});
export function makeNodeMap(posts: Post[]): Record<string, GraphNode> {
let nodes = {};
const nodes = {};
posts.forEach((p) => {
nodes[p.index] = { children: { empty: null }, post: p };
});

View File

@ -1,8 +1,8 @@
import { Post, GraphNode, TextContent, Graph, NodeMap } from "~/types";
import { Post, GraphNode, TextContent, Graph, NodeMap } from '@urbit/api';
import { buntPost } from '~/logic/lib/post';
import { unixToDa } from "~/logic/lib/util";
import {BigIntOrderedMap} from "./BigIntOrderedMap";
import bigInt, {BigInteger} from 'big-integer';
import { unixToDa } from '~/logic/lib/util';
import { BigIntOrderedMap } from './BigIntOrderedMap';
import bigInt, { BigInteger } from 'big-integer';
export function newPost(
title: string,
@ -12,20 +12,20 @@ export function newPost(
const nowDa = unixToDa(now);
const root: Post = {
author: `~${window.ship}`,
index: "/" + nowDa.toString(),
"time-sent": now,
index: '/' + nowDa.toString(),
'time-sent': now,
contents: [],
hash: null,
signatures: [],
signatures: []
};
const revContainer: Post = { ...root, index: root.index + "/1" };
const commentsContainer = { ...root, index: root.index + "/2" };
const revContainer: Post = { ...root, index: root.index + '/1' };
const commentsContainer = { ...root, index: root.index + '/2' };
const firstRevision: Post = {
...revContainer,
index: revContainer.index + "/1",
contents: [{ text: title }, { text: body }],
index: revContainer.index + '/1',
contents: [{ text: title }, { text: body }]
};
const nodes = {
@ -37,16 +37,16 @@ export function newPost(
children: {
1: {
post: firstRevision,
children: null,
},
},
children: null
}
}
},
2: {
post: commentsContainer,
children: null
},
},
},
}
}
}
};
return [nowDa, nodes];
@ -57,15 +57,15 @@ export function editPost(rev: number, noteId: BigInteger, title: string, body: s
const newRev: Post = {
author: `~${window.ship}`,
index: `/${noteId.toString()}/1/${rev}`,
"time-sent": now,
'time-sent': now,
contents: [{ text: title }, { text: body }],
hash: null,
signatures: [],
signatures: []
};
const nodes = {
[newRev.index]: {
post: newRev,
children: null
children: null
}
};
@ -74,7 +74,7 @@ export function editPost(rev: number, noteId: BigInteger, title: string, body: s
export function getLatestRevision(node: GraphNode): [number, string, string, Post] {
const revs = node.children.get(bigInt(1));
const empty = [1, "", "", buntPost()] as [number, string, string, Post];
const empty = [1, '', '', buntPost()] as [number, string, string, Post];
if(!revs) {
return empty;
}
@ -98,17 +98,16 @@ export function getLatestCommentRevision(node: GraphNode): [number, Post] {
return [revNum.toJSNumber(), rev.post];
}
export function getComments(node: GraphNode): GraphNode {
const comments = node.children.get(bigInt(2));
if(!comments) {
return { post: buntPost(), children: new BigIntOrderedMap() }
return { post: buntPost(), children: new BigIntOrderedMap() };
}
return comments;
}
export function getSnippet(body: string) {
const start = body.slice(0, body.indexOf('\n', 2));
return (start === body || start.startsWith("![")) ? start : `${start}...`;
return (start === body || start.startsWith('![')) ? start : `${start}...`;
}

View File

@ -1,16 +1,16 @@
import _ from "lodash";
import _ from 'lodash';
export const alignY = ["top", "bottom"] as const;
export const alignY = ['top', 'bottom'] as const;
export type AlignY = typeof alignY[number];
export const alignX = ["left", "right"] as const;
export const alignX = ['left', 'right'] as const;
export type AlignX = typeof alignX[number];
export function getRelativePosition(
relativeTo: HTMLElement | null,
alignX: AlignX | AlignX[],
alignY: AlignY | AlignY[],
offsetX: number = 0,
offsetY: number = 0
offsetX = 0,
offsetY = 0
) {
const rect = relativeTo?.getBoundingClientRect();
if (!rect) {
@ -20,7 +20,7 @@ export function getRelativePosition(
top: rect.top - offsetY,
left: rect.left - offsetX,
bottom: document.documentElement.clientHeight - rect.bottom - offsetY,
right: document.documentElement.clientWidth - rect.right - offsetX,
right: document.documentElement.clientWidth - rect.right - offsetX
};
const alignXArr = _.isArray(alignX) ? alignX : [alignX];
const alignYArr = _.isArray(alignY) ? alignY : [alignY];
@ -34,7 +34,7 @@ export function getRelativePosition(
[...Array(idx), `${bounds[a]}px`],
acc[a] || [],
(a, b) => a || b || null
),
)
}),
{}
),
@ -46,10 +46,10 @@ export function getRelativePosition(
[...Array(idx), `${bounds[a]}px`],
acc[a] || [],
(a, b) => a || b || null
),
)
}),
{}
),
)
} as Record<AlignY | AlignX, string[]>;
}

View File

@ -1,6 +1,6 @@
import { TutorialProgress, Associations } from "~/types";
import { AlignX, AlignY } from "~/logic/lib/relativePosition";
import { Direction } from "~/views/components/Triangle";
import { TutorialProgress, Associations } from '@urbit/api';
import { AlignX, AlignY } from '~/logic/lib/relativePosition';
import { Direction } from '~/views/components/Triangle';
export const MODAL_WIDTH = 256;
export const MODAL_HEIGHT = 256;
@ -43,7 +43,7 @@ export const getTrianglePosition = (dir: Direction) => {
return {
top: midY,
left: '-32px'
}
};
case 'North':
return {
top: '-32px',
@ -55,117 +55,117 @@ export const getTrianglePosition = (dir: Direction) => {
left: midX
};
}
}
};
export const progressDetails: Record<TutorialProgress, StepDetail> = {
hidden: {} as any,
exit: {} as any,
done: {
title: "End",
title: 'End',
description:
"This tutorial is finished. Would you like to leave Beginner Island?",
url: "/",
alignX: "right",
alignY: "top",
'This tutorial is finished. Would you like to leave Beginner Island?',
url: '/',
alignX: 'right',
alignY: 'top',
offsetX: MODAL_WIDTH + 8,
offsetY: 0,
offsetY: 0
},
start: {
title: "New Group added",
title: 'New Group added',
description:
"We just added you to the Beginner island group to show you around. This group is public, but other groups can be private",
url: "/",
alignX: "right",
alignY: "top",
arrow: "West",
'We just added you to the Beginner island group to show you around. This group is public, but other groups can be private',
url: '/',
alignX: 'right',
alignY: 'top',
arrow: 'West',
offsetX: MODAL_WIDTH + 24,
offsetY: 64,
offsetY: 64
},
"group-desc": {
title: "What's a group",
'group-desc': {
title: 'What\'s a group',
description:
"A group contains members and tends to be centered around a topic or multiple topics.",
'A group contains members and tends to be centered around a topic or multiple topics.',
url: `/~landscape/ship/${TUTORIAL_HOST}/${TUTORIAL_GROUP}`,
alignX: "left",
alignY: "top",
arrow: "East",
alignX: 'left',
alignY: 'top',
arrow: 'East',
offsetX: MODAL_WIDTH + 24,
offsetY: MODAL_HEIGHT / 2 - 8,
offsetY: MODAL_HEIGHT / 2 - 8
},
channels: {
title: "Channels",
title: 'Channels',
description:
"Inside a group you have three types of Channels: Chat, Collection, or Notebook. Mix and match these depending on your group context!",
'Inside a group you have three types of Channels: Chat, Collection, or Notebook. Mix and match these depending on your group context!',
url: `/~landscape/ship/${TUTORIAL_HOST}/${TUTORIAL_GROUP}`,
alignY: "top",
alignX: "right",
arrow: "West",
alignY: 'top',
alignX: 'right',
arrow: 'West',
offsetX: MODAL_WIDTH + 24,
offsetY: -8,
offsetY: -8
},
chat: {
title: "Chat",
title: 'Chat',
description:
"Chat channels are for messaging within your group. Direct Messages are also supported, and are accessible from the “DMs” tile on the homescreen",
'Chat channels are for messaging within your group. Direct Messages are also supported, and are accessible from the “DMs” tile on the homescreen',
url: `/~landscape/ship/${TUTORIAL_HOST}/${TUTORIAL_GROUP}/resource/chat/ship/${TUTORIAL_HOST}/${TUTORIAL_CHAT}`,
alignY: "top",
arrow: "North",
alignX: "right",
alignY: 'top',
arrow: 'North',
alignX: 'right',
offsetY: -56,
offsetX: -8,
offsetX: -8
},
link: {
title: "Collection",
title: 'Collection',
description:
"A collection is where you can share and view links, images, and other media within your group. Every item in a Collection can have its own comment thread.",
'A collection is where you can share and view links, images, and other media within your group. Every item in a Collection can have its own comment thread.',
url: `/~landscape/ship/${TUTORIAL_HOST}/${TUTORIAL_GROUP}/resource/link/ship/${TUTORIAL_HOST}/${TUTORIAL_LINKS}`,
alignY: "top",
alignX: "right",
arrow: "North",
alignY: 'top',
alignX: 'right',
arrow: 'North',
offsetX: -8,
offsetY: -56,
offsetY: -56
},
publish: {
title: "Notebook",
title: 'Notebook',
description:
"Notebooks are for creating long-form content within your group. Use markdown to create rich posts with headers, lists and images.",
'Notebooks are for creating long-form content within your group. Use markdown to create rich posts with headers, lists and images.',
url: `/~landscape/ship/${TUTORIAL_HOST}/${TUTORIAL_GROUP}/resource/publish/ship/${TUTORIAL_HOST}/${TUTORIAL_BOOK}`,
alignY: "top",
alignX: "right",
arrow: "North",
alignY: 'top',
alignX: 'right',
arrow: 'North',
offsetX: -8,
offsetY: -56,
offsetY: -56
},
notifications: {
title: "Notifications",
description: "You will get updates from subscribed channels and mentions here. You can access Notifications through Leap.",
title: 'Notifications',
description: 'You will get updates from subscribed channels and mentions here. You can access Notifications through Leap.',
url: '/~notifications',
alignY: "top",
alignX: "left",
arrow: "North",
alignY: 'top',
alignX: 'left',
arrow: 'North',
offsetX: (MODAL_WIDTH / 2) - 16,
offsetY: -48,
offsetY: -48
},
profile: {
title: "Profile",
title: 'Profile',
description:
"Your profile is customizable and can be shared with other ships. Enter as much or as little information as youd like.",
'Your profile is customizable and can be shared with other ships. Enter as much or as little information as youd like.',
url: `/~profile/~${window.ship}`,
alignY: "top",
alignX: "right",
arrow: "South",
alignY: 'top',
alignX: 'right',
arrow: 'South',
offsetX: -300 + MODAL_WIDTH / 2,
offsetY: -120 + MODAL_HEIGHT / 2,
offsetY: -120 + MODAL_HEIGHT / 2
},
leap: {
title: "Leap",
title: 'Leap',
description:
"Leap allows you to go to a specific channel, message, collection, profile or group simply by typing in a command or selecting a shortcut from the dropdown menu.",
'Leap allows you to go to a specific channel, message, collection, profile or group simply by typing in a command or selecting a shortcut from the dropdown menu.',
url: `/~profile/~${window.ship}`,
alignY: "top",
alignX: "left",
arrow: "North",
alignY: 'top',
alignX: 'left',
arrow: 'North',
offsetX: 0.3 *MODAL_HEIGHT,
offsetY: -48,
},
offsetY: -48
}
};

View File

@ -1,4 +1,4 @@
import { useState, useCallback, useMemo, useEffect } from "react";
import { useState, useCallback, useMemo, useEffect } from 'react';
function validateDragEvent(e: DragEvent): FileList | File[] | true | null {
const files: File[] = [];
@ -8,8 +8,8 @@ function validateDragEvent(e: DragEvent): FileList | File[] | true | null {
}
if (e.dataTransfer?.items) {
Array.from(e.dataTransfer.items || [])
.filter((i) => i.kind === 'file')
.forEach(f => {
.filter(i => i.kind === 'file')
.forEach((f) => {
valid = true; // Valid if file exists, but on DragOver, won't reveal its contents for security
const data = f.getAsFile();
if (data) {
@ -89,14 +89,14 @@ export function useFileDrag(dragged: (f: FileList | File[], e: DragEvent) => voi
document.body.addEventListener('mouseout', mouseleave);
return () => {
document.body.removeEventListener('mouseout', mouseleave);
}
};
}, []);
const bind = {
onDragLeave,
onDragOver,
onDrop,
onDragEnter,
onDragEnter
};
return { bind, dragging };

View File

@ -1,4 +1,4 @@
import { useState, useEffect, useMemo, useCallback } from "react";
import { useState, useEffect, useMemo, useCallback } from 'react';
export function useDropdown<C>(
candidates: C[],
@ -12,10 +12,10 @@ export function useDropdown<C>(
(s: string) => {
const exactMatch = isExact(s);
const exact = exactMatch ? [exactMatch] : [];
const opts = [...new Set([...exact, ...candidates.filter((c) => searchPred(s, c))])];
const opts = [...new Set([...exact, ...candidates.filter(c => searchPred(s, c))])];
setOptions(opts);
if (selected) {
const idx = opts.findIndex((c) => key(c) === key(selected));
const idx = opts.findIndex(c => key(c) === key(selected));
if (idx < 0) {
setSelected(undefined);
}
@ -29,9 +29,11 @@ export function useDropdown<C>(
const select = (idx: number) => {
setSelected(options[idx]);
};
if(!selected) { select(0); return false; }
if(!selected) {
select(0); return false;
}
const idx = options.findIndex((c) => key(c) === key(selected));
const idx = options.findIndex(c => key(c) === key(selected));
if (
idx === -1 ||
(options.length - 1 <= idx && !backward)
@ -55,6 +57,6 @@ export function useDropdown<C>(
back,
search,
selected,
options,
options
};
}

View File

@ -1,6 +1,5 @@
import { useEffect } from 'react';
import {useLocation} from "react-router-dom";
import { useLocation } from 'react-router-dom';
export function useHashLink() {
const location = useLocation();
@ -10,8 +9,5 @@ export function useHashLink() {
return;
}
document.querySelector(location.hash)?.scrollIntoView({ behavior: 'smooth', block: 'start' });
}, [location.hash]);
}

View File

@ -1,6 +1,6 @@
import { useEffect, RefObject, useRef, useState } from "react";
import _ from "lodash";
import usePreviousValue from "./usePreviousValue";
import { useEffect, RefObject, useRef, useState } from 'react';
import _ from 'lodash';
import usePreviousValue from './usePreviousValue';
export function distanceToBottom(el: HTMLElement) {
const { scrollTop, scrollHeight, clientHeight } = el;
@ -40,7 +40,6 @@ export function useLazyScroll(
}
}, [count]);
useEffect(() => {
if (!ref.current) {
return;
@ -54,13 +53,12 @@ export function useLazyScroll(
loadUntil(el);
};
ref.current.addEventListener("scroll", onScroll, { passive: true });
ref.current.addEventListener('scroll', onScroll, { passive: true });
return () => {
ref.current?.removeEventListener("scroll", onScroll);
ref.current?.removeEventListener('scroll', onScroll);
};
}, [ref?.current, count]);
return { isDone, isLoading };
}

View File

@ -1,4 +1,4 @@
import { useState, useCallback, useEffect } from "react";
import { useState, useCallback, useEffect } from 'react';
function retrieve<T>(key: string, initial: T): T {
const s = localStorage.getItem(key);
@ -25,7 +25,7 @@ export function useLocalStorageState<T>(key: string, initial: T) {
const setState = useCallback(
(s: SetState<T>) => {
const updated = typeof s === "function" ? s(state) : s;
const updated = typeof s === 'function' ? s(state) : s;
_setState(updated);
localStorage.setItem(key, JSON.stringify(updated));
},

View File

@ -5,15 +5,15 @@ import React, {
SyntheticEvent,
useMemo,
useEffect,
useRef,
} from "react";
useRef
} from 'react';
import { Box } from "@tlon/indigo-react";
import { useOutsideClick } from "./useOutsideClick";
import { ModalOverlay } from "~/views/components/ModalOverlay";
import {Portal} from "~/views/components/Portal";
import {ModalPortal} from "~/views/components/ModalPortal";
import {PropFunc} from "~/types";
import { Box } from '@tlon/indigo-react';
import { useOutsideClick } from './useOutsideClick';
import { ModalOverlay } from '~/views/components/ModalOverlay';
import { Portal } from '~/views/components/Portal';
import { ModalPortal } from '~/views/components/ModalPortal';
import { PropFunc } from '@urbit/api';
type ModalFunc = (dismiss: () => void) => JSX.Element;
interface UseModalProps {
@ -42,7 +42,7 @@ export function useModal(props: UseModalProps & PropFunc<typeof Box>): UseModalR
() =>
!modalShown
? null
: typeof modal === "function"
: typeof modal === 'function'
? modal(dismiss)
: modal,
[modalShown, modal, dismiss]
@ -59,7 +59,7 @@ export function useModal(props: UseModalProps & PropFunc<typeof Box>): UseModalR
bg="white"
borderRadius={2}
border={[0, 1]}
borderColor={["washedGray", "washedGray"]}
borderColor={['washedGray', 'washedGray']}
display="flex"
alignItems="stretch"
flexDirection="column"
@ -76,6 +76,6 @@ export function useModal(props: UseModalProps & PropFunc<typeof Box>): UseModalR
return {
showModal,
modal: modalComponent,
modal: modalComponent
};
}

View File

@ -1,8 +1,8 @@
import { useEffect, RefObject } from "react";
import { useEffect, RefObject } from 'react';
export function useOutsideClick(
ref: RefObject<HTMLElement | null | undefined>,
onClick: () => void,
onClick: () => void
) {
useEffect(() => {
function handleClick(event: MouseEvent) {
@ -16,17 +16,16 @@ export function useOutsideClick(
}
function handleKeyDown(ev) {
if(ev.key === "Escape") {
if(ev.key === 'Escape') {
onClick();
}
}
document.addEventListener("mousedown", handleClick);
document.addEventListener("keydown", handleKeyDown);
document.addEventListener('mousedown', handleClick);
document.addEventListener('keydown', handleKeyDown);
return () => {
document.removeEventListener("mousedown", handleClick);
document.removeEventListener("keydown", handleKeyDown);
document.removeEventListener('mousedown', handleClick);
document.removeEventListener('keydown', handleKeyDown);
};
}, [ref.current, onClick]);
}

View File

@ -1,7 +1,5 @@
import { useRef } from "react";
import { Primitive } from "~/types";
import { useRef } from 'react';
import { Primitive } from '@urbit/api';
export default function usePreviousValue<T extends Primitive>(value: T): T {
const prev = useRef<T | null>(null);

View File

@ -1,5 +1,5 @@
import { useMemo, useCallback } from "react";
import { useLocation } from "react-router-dom";
import { useMemo, useCallback } from 'react';
import { useLocation } from 'react-router-dom';
import _ from 'lodash';
export function useQuery() {
@ -25,6 +25,6 @@ export function useQuery() {
return {
query,
appendQuery,
appendQuery
};
}

View File

@ -1,7 +1,7 @@
import { useCallback, useMemo, useEffect, useRef, useState } from "react";
import { S3State } from "../../types/s3-update";
import S3 from "aws-sdk/clients/s3";
import { dateToDa, deSig } from "./util";
import { useCallback, useMemo, useEffect, useRef, useState } from 'react';
import { S3State } from '../../types/s3-update';
import S3 from 'aws-sdk/clients/s3';
import { dateToDa, deSig } from './util';
export interface IuseS3 {
canUpload: boolean;
@ -28,14 +28,14 @@ const useS3 = (s3: S3State, { accept = '*' } = { accept: '*' }): IuseS3 => {
const canUpload = useMemo(
() =>
(client && s3.credentials && s3.configuration.currentBucket !== "") || false,
(client && s3.credentials && s3.configuration.currentBucket !== '') || false,
[s3.credentials, s3.configuration.currentBucket, client]
);
const upload = useCallback(
async (file: File, bucket: string) => {
if (!client.current) {
throw new Error("S3 not ready");
throw new Error('S3 not ready');
}
const fileParts = file.name.split('.');
@ -47,8 +47,8 @@ const useS3 = (s3: S3State, { accept = '*' } = { accept: '*' }): IuseS3 => {
Bucket: bucket,
Key: `${window.ship}/${timestamp}-${fileName}.${fileExtension}`,
Body: file,
ACL: "public-read",
ContentType: file.type,
ACL: 'public-read',
ContentType: file.type
};
setUploading(true);
@ -63,8 +63,8 @@ const useS3 = (s3: S3State, { accept = '*' } = { accept: '*' }): IuseS3 => {
);
const uploadDefault = useCallback(async (file: File) => {
if (s3.configuration.currentBucket === "") {
throw new Error("current bucket not set");
if (s3.configuration.currentBucket === '') {
throw new Error('current bucket not set');
}
return upload(file, s3.configuration.currentBucket);
}, [s3]);
@ -84,11 +84,10 @@ const useS3 = (s3: S3State, { accept = '*' } = { accept: '*' }): IuseS3 => {
}
uploadDefault(files[0]).then(resolve);
document.body.removeChild(fileSelector);
})
});
document.body.appendChild(fileSelector);
fileSelector.click();
})
});
},
[uploadDefault]
);
@ -96,4 +95,4 @@ const useS3 = (s3: S3State, { accept = '*' } = { accept: '*' }): IuseS3 => {
return { canUpload, upload, uploadDefault, uploading, promptUpload };
};
export default useS3;
export default useS3;

View File

@ -1,23 +1,23 @@
import { MouseEvent, useCallback, useState, useEffect } from "react";
export type AsyncClickableState = "waiting" | "error" | "loading" | "success";
import { MouseEvent, useCallback, useState, useEffect } from 'react';
export type AsyncClickableState = 'waiting' | 'error' | 'loading' | 'success';
export function useStatelessAsyncClickable(
onClick: (e: MouseEvent) => Promise<void>,
name: string
) {
const [state, setState] = useState<ButtonState>("waiting");
const [state, setState] = useState<ButtonState>('waiting');
const handleClick = useCallback(
async (e: MouseEvent) => {
try {
setState("loading");
setState('loading');
await onClick(e);
setState("success");
setState('success');
} catch (e) {
console.error(e);
setState("error");
setState('error');
} finally {
setTimeout(() => {
setState("waiting");
setState('waiting');
}, 3000);
}
},
@ -26,7 +26,7 @@ export function useStatelessAsyncClickable(
// When name changes, reset button
useEffect(() => {
setState("waiting");
setState('waiting');
}, [name]);
return { buttonState: state, onClick: handleClick };

View File

@ -1,12 +1,11 @@
import { useState, useEffect, useCallback } from 'react';
export function useWaitForProps<P>(props: P, timeout: number = 0) {
export function useWaitForProps<P>(props: P, timeout = 0) {
const [resolve, setResolve] = useState<() => void>(() => () => {});
const [ready, setReady] = useState<(p: P) => boolean | undefined>();
useEffect(() => {
if (typeof ready === "function" && ready(props)) {
if (typeof ready === 'function' && ready(props)) {
resolve();
}
}, [props, ready, resolve]);
@ -26,7 +25,7 @@ export function useWaitForProps<P>(props: P, timeout: number = 0) {
setResolve(() => resolve);
if(timeout > 0) {
setTimeout(() => {
reject(new Error("Timed out"));
reject(new Error('Timed out'));
}, timeout);
}
});

View File

@ -1,27 +1,27 @@
import { useEffect, useState } from 'react';
import _ from "lodash";
import f, { memoize } from "lodash/fp";
import bigInt, { BigInteger } from "big-integer";
import { Contact } from '~/types';
import _ from 'lodash';
import f, { memoize } from 'lodash/fp';
import bigInt, { BigInteger } from 'big-integer';
import { Contact } from '@urbit/api';
import useLocalState from '../state/local';
export const MOBILE_BROWSER_REGEX = /Android|webOS|iPhone|iPad|iPod|BlackBerry/i;
export const MOMENT_CALENDAR_DATE = {
sameDay: "[Today]",
nextDay: "[Tomorrow]",
nextWeek: "dddd",
lastDay: "[Yesterday]",
lastWeek: "[Last] dddd",
sameElse: "~YYYY.M.D",
sameDay: '[Today]',
nextDay: '[Tomorrow]',
nextWeek: 'dddd',
lastDay: '[Yesterday]',
lastWeek: '[Last] dddd',
sameElse: '~YYYY.M.D'
};
export const getModuleIcon = (mod: string) => {
if (mod === "link") {
return "Collection";
if (mod === 'link') {
return 'Collection';
}
return _.capitalize(mod);
}
};
export function wait(ms: number) {
return new Promise((resolve, reject) => {
@ -37,8 +37,8 @@ export function parentPath(path: string) {
return _.dropRight(path.split('/'), 1).join('/');
}
const DA_UNIX_EPOCH = bigInt("170141184475152167957503069145530368000"); // `@ud` ~1970.1.1
const DA_SECOND = bigInt("18446744073709551616"); // `@ud` ~s1
const DA_UNIX_EPOCH = bigInt('170141184475152167957503069145530368000'); // `@ud` ~1970.1.1
const DA_SECOND = bigInt('18446744073709551616'); // `@ud` ~s1
export function daToUnix(da: BigInteger) {
// ported from +time:enjs:format in hoon.hoon
const offset = DA_SECOND.divide(bigInt(2000));
@ -59,20 +59,20 @@ export function makePatDa(patda: string) {
}
export function udToDec(ud: string): string {
return ud.replace(/\./g, "");
return ud.replace(/\./g, '');
}
export function decToUd(str: string): string {
return _.trimStart(
f.flow(
f.split(""),
f.split(''),
f.reverse,
f.chunk(3),
f.map(f.flow(f.reverse, f.join(""))),
f.map(f.flow(f.reverse, f.join(''))),
f.reverse,
f.join(".")
f.join('.')
)(str),
"0."
'0.'
);
}
@ -86,12 +86,12 @@ export function clamp(x: number, min: number, max: number) {
// color is a #000000 color
export function adjustHex(color: string, amount: number): string {
return f.flow(
f.split(""),
f.split(''),
f.chunk(2), // get RGB channels
f.map((c) => parseInt(c.join(""), 16)), // as hex
f.map((c) => clamp(c + amount, 0, 255).toString(16)), // adjust
f.join(""),
(res) => `#${res}` //format
f.map(c => parseInt(c.join(''), 16)), // as hex
f.map(c => clamp(c + amount, 0, 255).toString(16)), // adjust
f.join(''),
res => `#${res}` // format
)(color.slice(1));
}
@ -101,12 +101,12 @@ export function resourceAsPath(resource: any) {
}
export function uuid() {
let str = "0v";
str += Math.ceil(Math.random() * 8) + ".";
let str = '0v';
str += Math.ceil(Math.random() * 8) + '.';
for (let i = 0; i < 5; i++) {
let _str = Math.ceil(Math.random() * 10000000).toString(32);
_str = ("00000" + _str).substr(-5, 5);
str += _str + ".";
_str = ('00000' + _str).substr(-5, 5);
str += _str + '.';
}
return str.slice(0, -1);
@ -120,11 +120,11 @@ export function uuid() {
*/
export function daToDate(st: string) {
const dub = function (n: string) {
return parseInt(n) < 10 ? "0" + parseInt(n) : n.toString();
return parseInt(n) < 10 ? '0' + parseInt(n) : n.toString();
};
const da = st.split("..");
const bigEnd = da[0].split(".");
const lilEnd = da[1].split(".");
const da = st.split('..');
const bigEnd = da[0].split('.');
const lilEnd = da[1].split('.');
const ds = `${bigEnd[0].slice(1)}-${dub(bigEnd[1])}-${dub(bigEnd[2])}T${dub(
lilEnd[0]
)}:${dub(lilEnd[1])}:${dub(lilEnd[2])}Z`;
@ -138,9 +138,9 @@ export function daToDate(st: string) {
~2018.7.17..23.15.09..5be5 // urbit @da
*/
export function dateToDa(d: Date, mil: boolean = false) {
export function dateToDa(d: Date, mil = false) {
const fil = function (n: number) {
return n >= 10 ? n : "0" + n;
return n >= 10 ? n : '0' + n;
};
return (
`~${d.getUTCFullYear()}.` +
@ -149,7 +149,7 @@ export function dateToDa(d: Date, mil: boolean = false) {
`${fil(d.getUTCHours())}.` +
`${fil(d.getUTCMinutes())}.` +
`${fil(d.getUTCSeconds())}` +
`${mil ? "..0000" : ""}`
`${mil ? '..0000' : ''}`
);
}
@ -157,16 +157,16 @@ export function deSig(ship: string) {
if (!ship) {
return null;
}
return ship.replace("~", "");
return ship.replace('~', '');
}
export function uxToHex(ux: string) {
if (ux.length > 2 && ux.substr(0, 2) === "0x") {
const value = ux.substr(2).replace(".", "").padStart(6, "0");
if (ux.length > 2 && ux.substr(0, 2) === '0x') {
const value = ux.substr(2).replace('.', '').padStart(6, '0');
return value;
}
const value = ux.replace(".", "").padStart(6, "0");
const value = ux.replace('.', '').padStart(6, '0');
return value;
}
@ -187,13 +187,13 @@ export function writeText(str: string) {
let success = false;
function listener(e) {
e.clipboardData.setData("text/plain", str);
e.clipboardData.setData('text/plain', str);
e.preventDefault();
success = true;
}
document.addEventListener("copy", listener);
document.execCommand("copy");
document.removeEventListener("copy", listener);
document.addEventListener('copy', listener);
document.execCommand('copy');
document.removeEventListener('copy', listener);
document?.getSelection()?.removeAllRanges();
@ -206,21 +206,21 @@ export function writeText(str: string) {
// trim patps to match dojo, chat-cli
export function cite(ship: string) {
let patp = ship,
shortened = "";
if (patp === null || patp === "") {
shortened = '';
if (patp === null || patp === '') {
return null;
}
if (patp.startsWith("~")) {
if (patp.startsWith('~')) {
patp = patp.substr(1);
}
// comet
if (patp.length === 56) {
shortened = "~" + patp.slice(0, 6) + "_" + patp.slice(50, 56);
shortened = '~' + patp.slice(0, 6) + '_' + patp.slice(50, 56);
return shortened;
}
// moon
if (patp.length === 27) {
shortened = "~" + patp.slice(14, 20) + "^" + patp.slice(21, 27);
shortened = '~' + patp.slice(14, 20) + '^' + patp.slice(21, 27);
return shortened;
}
return `~${patp}`;
@ -232,7 +232,6 @@ export function alphabeticalOrder(a: string, b: string) {
export function lengthOrder(a: string, b: string) {
return b.length - a.length;
}
// TODO: deprecated
@ -244,13 +243,13 @@ export function alphabetiseAssociations(associations: any) {
let bName = b.substr(1);
if (associations[a].metadata && associations[a].metadata.title) {
aName =
associations[a].metadata.title !== ""
associations[a].metadata.title !== ''
? associations[a].metadata.title
: a.substr(1);
}
if (associations[b].metadata && associations[b].metadata.title) {
bName =
associations[b].metadata.title !== ""
associations[b].metadata.title !== ''
? associations[b].metadata.title
: b.substr(1);
}
@ -266,41 +265,42 @@ export function alphabetiseAssociations(associations: any) {
// for example, 'some Chars!' becomes '~.some.~43.hars~21.'
//
export function stringToTa(str: string) {
let out = "";
let out = '';
for (let i = 0; i < str.length; i++) {
const char = str[i];
let add = "";
let add = '';
switch (char) {
case " ":
add = ".";
case ' ':
add = '.';
break;
case ".":
add = "~.";
case '.':
add = '~.';
break;
case "~":
add = "~~";
case '~':
add = '~~';
break;
default:
const charCode = str.charCodeAt(i);
if (
(charCode >= 97 && charCode <= 122) || // a-z
(charCode >= 48 && charCode <= 57) || // 0-9
char === "-"
char === '-'
) {
add = char;
} else {
// TODO behavior for unicode doesn't match +wood's,
// but we can probably get away with that for now.
add = "~" + charCode.toString(16) + ".";
add = '~' + charCode.toString(16) + '.';
}
}
out = out + add;
}
return "~." + out;
return '~.' + out;
}
export function amOwnerOfGroup(groupPath: string) {
if (!groupPath) return false;
if (!groupPath)
return false;
const groupOwner = /(\/~)?\/~([a-z-]{3,})\/.*/.exec(groupPath)?.[2];
return window.ship === groupOwner;
}
@ -308,18 +308,18 @@ export function amOwnerOfGroup(groupPath: string) {
export function getContactDetails(contact: any) {
const member = !contact;
contact = contact || {
nickname: "",
nickname: '',
avatar: null,
color: "0x0",
color: '0x0'
};
const nickname = contact.nickname || "";
const color = uxToHex(contact.color || "0x0");
const nickname = contact.nickname || '';
const color = uxToHex(contact.color || '0x0');
const avatar = contact.avatar || null;
return { nickname, color, member, avatar };
}
export function stringToSymbol(str: string) {
let result = "";
let result = '';
for (let i = 0; i < str.length; i++) {
const n = str.charCodeAt(i);
if ((n >= 97 && n <= 122) || (n >= 48 && n <= 57)) {
@ -327,19 +327,17 @@ export function stringToSymbol(str: string) {
} else if (n >= 65 && n <= 90) {
result += String.fromCharCode(n + 32);
} else {
result += "-";
result += '-';
}
}
result = result.replace(/^[\-\d]+|\-+/g, "-");
result = result.replace(/^\-+|\-+$/g, "");
if (result === "") {
result = result.replace(/^[\-\d]+|\-+/g, '-');
result = result.replace(/^\-+|\-+$/g, '');
if (result === '') {
return dateToDa(new Date());
}
return result;
}
/**
* Formats a numbers as a `@ud` inserting dot where needed
*/
@ -351,23 +349,24 @@ export function numToUd(num: number) {
f.reverse,
f.map(s => s.join('')),
f.join('.')
)(num.toString())
)(num.toString());
}
export function usePreventWindowUnload(shouldPreventDefault: boolean, message = "You have unsaved changes. Are you sure you want to exit?") {
export function usePreventWindowUnload(shouldPreventDefault: boolean, message = 'You have unsaved changes. Are you sure you want to exit?') {
useEffect(() => {
if (!shouldPreventDefault) return;
const handleBeforeUnload = event => {
if (!shouldPreventDefault)
return;
const handleBeforeUnload = (event) => {
event.preventDefault();
return message;
}
window.addEventListener("beforeunload", handleBeforeUnload);
};
window.addEventListener('beforeunload', handleBeforeUnload);
window.onbeforeunload = handleBeforeUnload;
return () => {
window.removeEventListener("beforeunload", handleBeforeUnload);
window.removeEventListener('beforeunload', handleBeforeUnload);
// @ts-ignore
window.onbeforeunload = undefined;
}
};
}, [shouldPreventDefault]);
}
@ -378,7 +377,7 @@ export function pluralize(text: string, isPlural = false, vowel = false) {
// 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);
return Boolean(contact && contact.nickname && !hideNicknames);
}
interface useHoveringInterface {
@ -406,7 +405,6 @@ export function getItemTitle(association: Association) {
return cite(`~${name.slice(4)}`);
}
return cite(ship);
}
return association.metadata.title || association.resource
};
return association.metadata.title || association.resource;
}

View File

@ -1,24 +1,24 @@
import { Associations, Workspace } from "~/types";
import { Associations, Workspace } from '@urbit/api';
export function getTitleFromWorkspace(
associations: Associations,
workspace: Workspace
) {
switch (workspace.type) {
case "home":
return "My Channels";
case "messages":
return "Messages";
case "group":
case 'home':
return 'My Channels';
case 'messages':
return 'Messages';
case 'group':
const association = associations.groups[workspace.group];
return association?.metadata?.title || "";
return association?.metadata?.title || '';
}
}
export function getGroupFromWorkspace(
workspace: Workspace
): string | undefined {
if (workspace.type === "group") {
if (workspace.type === 'group') {
return workspace.group;
}

View File

@ -1,8 +1,8 @@
import _ from 'lodash';
import { StoreState } from '../../store/type';
import { Cage } from '~/types/cage';
import { ContactUpdate } from '~/types/contact-update';
import {resourceAsPath} from '../lib/util';
import { ContactUpdate } from '@urbit/api/contacts';
import { resourceAsPath } from '../lib/util';
type ContactState = Pick<StoreState, 'contacts'>;
@ -62,9 +62,9 @@ const edit = (json: ContactUpdate, state: S) => {
const contact = state.contacts?.[ship];
const value = data['edit-field'][field];
if(!contact) {
return;
return;
}
if(field === 'add-group') {
contact.groups.push(value);
} else if (field === 'remove-group') {
@ -80,4 +80,3 @@ const setPublic = (json: ContactUpdate, state: S) => {
state.isContactPublic = data;
};

View File

@ -10,9 +10,9 @@ import {
OpenPolicyDiff,
OpenPolicy,
InvitePolicyDiff,
InvitePolicy,
} from '~/types/group-update';
import { Enc, PatpNoSig } from '~/types/noun';
InvitePolicy
} from '@urbit/api/groups';
import { Enc, PatpNoSig } from '@urbit/api';
import { resourceAsPath } from '../lib/util';
type GroupState = Pick<StoreState, 'groups' | 'groupKeys'>;
@ -23,7 +23,7 @@ function decodeGroup(group: Enc<Group>): Group {
...group,
members,
tags: decodeTags(group.tags),
policy: decodePolicy(group.policy),
policy: decodePolicy(group.policy)
};
return res;
}
@ -35,7 +35,7 @@ function decodePolicy(policy: Enc<GroupPolicy>): GroupPolicy {
} else {
const { open } = policy;
return {
open: { banned: new Set(open.banned), banRanks: new Set(open.banRanks) },
open: { banned: new Set(open.banned), banRanks: new Set(open.banRanks) }
};
}
}
@ -98,7 +98,7 @@ export default class GroupReducer<S extends GroupState> {
members: new Set(),
tags: { role: { admin: new Set([window.ship]) } },
policy: decodePolicy(policy),
hidden,
hidden
};
}
}
@ -189,7 +189,6 @@ export default class GroupReducer<S extends GroupState> {
}
}
private inviteChangePolicy(diff: InvitePolicyDiff, policy: InvitePolicy) {
if ('addInvites' in diff) {
const { addInvites } = diff;

View File

@ -1,25 +1,24 @@
import { resourceAsPath } from "~/logic/lib/util";
import { resourceAsPath } from '~/logic/lib/util';
const initial = (json: any, state: any) => {
const data = json.initial;
if(data) {
state.pendingJoin = data;
}
}
};
const progress = (json: any, state: any) => {
const data = json.progress;
if(data) {
const { progress, resource } = data;
state.pendingJoin = {...state.pendingJoin, [resource]: progress };
state.pendingJoin = { ...state.pendingJoin, [resource]: progress };
if(progress === 'done') {
setTimeout(() => {
delete state.pendingJoin[resource];
}, 10000);
}
}
}
};
export const GroupViewReducer = (json: any, state: any) => {
const data = json['group-view-update'];
@ -27,4 +26,4 @@ export const GroupViewReducer = (json: any, state: any) => {
progress(data, state);
initial(data, state);
}
}
};

View File

@ -3,22 +3,21 @@ import {
NotifIndex,
NotificationGraphConfig,
GroupNotificationsConfig,
UnreadStats,
} from "~/types";
import { makePatDa } from "~/logic/lib/util";
import _ from "lodash";
import {StoreState} from "../store/type";
UnreadStats
} from '@urbit/api';
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" >;
type HarkState = Pick<StoreState, 'notifications' | 'notificationsGraphConfig' | 'notificationsGroupConfig' | 'unreads' >;
export const HarkReducer = (json: any, state: HarkState) => {
const data = _.get(json, "harkUpdate", false);
const data = _.get(json, 'harkUpdate', false);
if (data) {
reduce(data, state);
}
const graphHookData = _.get(json, "hark-graph-hook-update", false);
const graphHookData = _.get(json, 'hark-graph-hook-update', false);
if (graphHookData) {
graphInitial(graphHookData, state);
graphIgnore(graphHookData, state);
@ -26,7 +25,7 @@ export const HarkReducer = (json: any, state: HarkState) => {
graphWatchSelf(graphHookData, state);
graphMentions(graphHookData, state);
}
const groupHookData = _.get(json, "hark-group-hook-update", false);
const groupHookData = _.get(json, 'hark-group-hook-update', false);
if (groupHookData) {
groupInitial(groupHookData, state);
groupListen(groupHookData, state);
@ -35,31 +34,31 @@ export const HarkReducer = (json: any, state: HarkState) => {
};
function groupInitial(json: any, state: HarkState) {
const data = _.get(json, "initial", false);
const data = _.get(json, 'initial', false);
if (data) {
state.notificationsGroupConfig = data;
}
}
function graphInitial(json: any, state: HarkState) {
const data = _.get(json, "initial", false);
const data = _.get(json, 'initial', false);
if (data) {
state.notificationsGraphConfig = data;
}
}
function graphListen(json: any, state: HarkState) {
const data = _.get(json, "listen", false);
const data = _.get(json, 'listen', false);
if (data) {
state.notificationsGraphConfig.watching = [
...state.notificationsGraphConfig.watching,
data,
data
];
}
}
function graphIgnore(json: any, state: HarkState) {
const data = _.get(json, "ignore", false);
const data = _.get(json, 'ignore', false);
if (data) {
state.notificationsGraphConfig.watching = state.notificationsGraphConfig.watching.filter(
({ graph, index }) => !(graph === data.graph && index === data.index)
@ -68,30 +67,30 @@ function graphIgnore(json: any, state: HarkState) {
}
function groupListen(json: any, state: HarkState) {
const data = _.get(json, "listen", false);
const data = _.get(json, 'listen', false);
if (data) {
state.notificationsGroupConfig = [...state.notificationsGroupConfig, data];
}
}
function groupIgnore(json: any, state: HarkState) {
const data = _.get(json, "ignore", false);
const data = _.get(json, 'ignore', false);
if (data) {
state.notificationsGroupConfig = state.notificationsGroupConfig.filter(
(n) => n !== data
n => n !== data
);
}
}
function graphMentions(json: any, state: HarkState) {
const data = _.get(json, "set-mentions", undefined);
const data = _.get(json, 'set-mentions', undefined);
if (!_.isUndefined(data)) {
state.notificationsGraphConfig.mentions = data;
}
}
function graphWatchSelf(json: any, state: HarkState) {
const data = _.get(json, "set-watch-on-self", undefined);
const data = _.get(json, 'set-watch-on-self', undefined);
if (!_.isUndefined(data)) {
state.notificationsGraphConfig.watchOnSelf = data;
}
@ -131,14 +130,14 @@ function seenIndex(json: any, state: HarkState) {
function readEach(json: any, state: HarkState) {
const data = _.get(json, 'read-each');
if(data) {
updateUnreads(state, data.index, u => u.delete(data.target))
updateUnreads(state, data.index, u => u.delete(data.target));
}
}
function readSince(json: any, state: HarkState) {
const data = _.get(json, 'read-count');
if(data) {
updateUnreadCount(state, data, () => 0)
updateUnreadCount(state, data, () => 0);
}
}
@ -152,7 +151,7 @@ function unreadSince(json: any, state: HarkState) {
function unreadEach(json: any, state: HarkState) {
const data = _.get(json, 'unread-each');
if(data) {
updateUnreads(state, data.index, us => us.add(data.target))
updateUnreads(state, data.index, us => us.add(data.target));
}
}
@ -175,15 +174,15 @@ function unreads(json: any, state: HarkState) {
}
}
function clearState(state){
let initialState = {
function clearState(state) {
const initialState = {
notifications: new BigIntOrderedMap<Timebox>(),
archivedNotifications: new BigIntOrderedMap<Timebox>(),
notificationsGroupConfig: [],
notificationsGraphConfig: {
watchOnSelf: false,
mentions: false,
watching: [],
watching: []
},
unreads: {
graph: {},
@ -192,7 +191,7 @@ function clearState(state){
notificationsCount: 0
};
Object.keys(initialState).forEach(key => {
Object.keys(initialState).forEach((key) => {
state[key] = initialState[key];
});
}
@ -203,7 +202,7 @@ function updateUnreadCount(state: HarkState, index: NotifIndex, count: (c: numbe
}
const property = [index.graph.graph, index.graph.index, 'unreads'];
const curr = _.get(state.unreads.graph, property, 0);
const newCount = count(curr)
const newCount = count(curr);
_.set(state.unreads.graph, property, newCount);
}
@ -218,7 +217,6 @@ function updateUnreads(state: HarkState, index: NotifIndex, f: (us: Set<string>)
_.set(state.unreads.graph, [index.graph.graph, index.graph.index, 'unreads'], unreads);
}
function updateNotificationStats(state: HarkState, index: NotifIndex, statField: 'notifications' | 'unreads' | 'last', f: (x: number) => number) {
if(statField === 'notifications') {
state.notificationsCount = f(state.notificationsCount);
@ -233,13 +231,13 @@ function updateNotificationStats(state: HarkState, index: NotifIndex, statField:
}
function added(json: any, state: HarkState) {
const data = _.get(json, "added", false);
const data = _.get(json, 'added', false);
if (data) {
const { index, notification } = data;
const time = makePatDa(data.time);
const timebox = state.notifications.get(time) || [];
const arrIdx = timebox.findIndex((idxNotif) =>
const arrIdx = timebox.findIndex(idxNotif =>
notifIdxEqual(index, idxNotif.index)
);
if (arrIdx !== -1) {
@ -256,14 +254,14 @@ function added(json: any, state: HarkState) {
}
const dnd = (json: any, state: HarkState) => {
const data = _.get(json, "set-dnd", undefined);
const data = _.get(json, 'set-dnd', undefined);
if (!_.isUndefined(data)) {
state.doNotDisturb = data;
}
};
const timebox = (json: any, state: HarkState) => {
const data = _.get(json, "timebox", false);
const data = _.get(json, 'timebox', false);
if (data) {
const time = makePatDa(data.time);
if (!data.archive) {
@ -273,21 +271,21 @@ const timebox = (json: any, state: HarkState) => {
};
function more(json: any, state: HarkState) {
const data = _.get(json, "more", false);
const data = _.get(json, 'more', false);
if (data) {
_.forEach(data, (d) => reduce(d, state));
_.forEach(data, d => reduce(d, state));
}
}
function notifIdxEqual(a: NotifIndex, b: NotifIndex) {
if ("graph" in a && "graph" in b) {
if ('graph' in a && 'graph' in b) {
return (
a.graph.graph === b.graph.graph &&
a.graph.group === b.graph.group &&
a.graph.module === b.graph.module &&
a.graph.description === b.graph.description
);
} else if ("group" in a && "group" in b) {
} else if ('group' in a && 'group' in b) {
return (
a.group.group === b.group.group &&
a.group.description === b.group.description
@ -305,14 +303,14 @@ function setRead(
const patDa = makePatDa(time);
const timebox = state.notifications.get(patDa);
if (_.isNull(timebox)) {
console.warn("Modifying nonexistent timebox");
console.warn('Modifying nonexistent timebox');
return;
}
const arrIdx = timebox.findIndex((idxNotif) =>
const arrIdx = timebox.findIndex(idxNotif =>
notifIdxEqual(index, idxNotif.index)
);
if (arrIdx === -1) {
console.warn("Modifying nonexistent index");
console.warn('Modifying nonexistent index');
return;
}
timebox[arrIdx].notification.read = read;
@ -320,7 +318,7 @@ function setRead(
}
function read(json: any, state: HarkState) {
const data = _.get(json, "read-note", false);
const data = _.get(json, 'read-note', false);
if (data) {
const { time, index } = data;
updateNotificationStats(state, index, 'notifications', x => x-1);
@ -329,7 +327,7 @@ function read(json: any, state: HarkState) {
}
function unread(json: any, state: HarkState) {
const data = _.get(json, "unread-note", false);
const data = _.get(json, 'unread-note', false);
if (data) {
const { time, index } = data;
updateNotificationStats(state, index, 'notifications', x => x+1);
@ -338,16 +336,16 @@ function unread(json: any, state: HarkState) {
}
function archive(json: any, state: HarkState) {
const data = _.get(json, "archive", false);
const data = _.get(json, 'archive', false);
if (data) {
const { index } = data;
const time = makePatDa(data.time);
const timebox = state.notifications.get(time);
if (!timebox) {
console.warn("Modifying nonexistent timebox");
console.warn('Modifying nonexistent timebox');
return;
}
const [archived, unarchived] = _.partition(timebox, (idxNotif) =>
const [archived, unarchived] = _.partition(timebox, idxNotif =>
notifIdxEqual(index, idxNotif.index)
);
if(unarchived.length === 0) {
@ -357,6 +355,6 @@ function archive(json: any, state: HarkState) {
state.notifications.set(time, unarchived);
}
const newlyRead = archived.filter(x => !x.notification.read).length;
updateNotificationStats(state, index, 'notifications', (x) => x - newlyRead);
updateNotificationStats(state, index, 'notifications', x => x - newlyRead);
}
}

View File

@ -1,10 +1,9 @@
import _ from 'lodash';
import { StoreState } from '../../store/type';
import { Cage } from '~/types/cage';
import { InviteUpdate } from '~/types/invite-update';
type InviteState = Pick<StoreState, "invites">;
import { InviteUpdate } from '@urbit/api/invite';
type InviteState = Pick<StoreState, 'invites'>;
export default class InviteReducer<S extends InviteState> {
reduce(json: Cage, state: S) {

View File

@ -51,12 +51,11 @@ export default class LaunchReducer<S extends LaunchState> {
changeIsShown(json: LaunchUpdate, state: S) {
const data = _.get(json, 'changeIsShown', false);
if (data) {
let tile = state.launch.tiles[data.name];
const tile = state.launch.tiles[data.name];
console.log(tile);
if (tile) {
tile.isShown = data.isShown;
}
}
}
}

View File

@ -2,14 +2,14 @@ import _ from 'lodash';
import { StoreState } from '../../store/type';
import { MetadataUpdate } from '~/types/metadata-update';
import { MetadataUpdate } from '@urbit/api/metadata';
import { Cage } from '~/types/cage';
type MetadataState = Pick<StoreState, 'associations'>;
export default class MetadataReducer<S extends MetadataState> {
reduce(json: Cage, state: S) {
let data = json['metadata-update']
const data = json['metadata-update'];
if (data) {
console.log(data);
this.associations(data, state);
@ -29,13 +29,13 @@ export default class MetadataReducer<S extends MetadataState> {
}
associations(json: MetadataUpdate, state: S) {
let data = _.get(json, 'associations', false);
const data = _.get(json, 'associations', false);
if (data) {
let metadata = state.associations;
const metadata = state.associations;
Object.keys(data).forEach((key) => {
let val = data[key];
let appName = val['app-name'];
let rid = val.resource;
const val = data[key];
const appName = val['app-name'];
const rid = val.resource;
if (!(appName in metadata)) {
metadata[appName] = {};
}
@ -50,11 +50,11 @@ export default class MetadataReducer<S extends MetadataState> {
}
add(json: MetadataUpdate, state: S) {
let data = _.get(json, 'add', false);
const data = _.get(json, 'add', false);
if (data) {
let metadata = state.associations;
let appName = data['app-name'];
let appPath = data.resource;
const metadata = state.associations;
const appName = data['app-name'];
const appPath = data.resource;
if (!(appName in metadata)) {
metadata[appName] = {};
@ -69,11 +69,11 @@ export default class MetadataReducer<S extends MetadataState> {
}
update(json: MetadataUpdate, state: S) {
let data = _.get(json, 'update-metadata', false);
const data = _.get(json, 'update-metadata', false);
if (data) {
let metadata = state.associations;
let appName = data['app-name'];
let rid = data.resource;
const metadata = state.associations;
const appName = data['app-name'];
const rid = data.resource;
if (!(appName in metadata)) {
metadata[appName] = {};
@ -88,11 +88,11 @@ export default class MetadataReducer<S extends MetadataState> {
}
remove(json: MetadataUpdate, state: S) {
let data = _.get(json, 'remove', false);
const data = _.get(json, 'remove', false);
if (data) {
let metadata = state.associations;
let appName = data['app-name'];
let rid = data.resource;
const metadata = state.associations;
const appName = data['app-name'];
const rid = data.resource;
if (appName in metadata && rid in metadata[appName]) {
delete metadata[appName][rid];

View File

@ -1,21 +1,21 @@
import _ from 'lodash';
import { StoreState } from '../../store/type';
import {
SettingsUpdate,
} from '~/types/settings';
SettingsUpdate
} from '@urbit/api/settings';
type SettingsState = Pick<StoreState, 'settings'>;
export default class SettingsReducer<S extends SettingsState>{
export default class SettingsReducer<S extends SettingsState> {
reduce(json: Cage, state: S) {
let data = json["settings-event"];
let data = json['settings-event'];
if (data) {
this.putBucket(data, state);
this.delBucket(data, state);
this.putEntry(data, state);
this.delEntry(data, state);
}
data = json["settings-data"];
data = json['settings-data'];
if (data) {
this.getAll(data, state);
this.getBucket(data, state);
@ -26,31 +26,31 @@ export default class SettingsReducer<S extends SettingsState>{
putBucket(json: SettingsUpdate, state: S) {
const data = _.get(json, 'put-bucket', false);
if (data) {
state.settings[data["bucket-key"]] = data.bucket;
state.settings[data['bucket-key']] = data.bucket;
}
}
delBucket(json: SettingsUpdate, state: S) {
const data = _.get(json, 'del-bucket', false);
if (data) {
delete state.settings[data["bucket-key"]];
delete state.settings[data['bucket-key']];
}
}
putEntry(json: SettingsUpdate, state: S) {
const data = _.get(json, 'put-entry', false);
if (data) {
if (!state.settings[data["bucket-key"]]) {
state.settings[data["bucket-key"]] = {};
if (!state.settings[data['bucket-key']]) {
state.settings[data['bucket-key']] = {};
}
state.settings[data["bucket-key"]][data["entry-key"]] = data.value;
state.settings[data['bucket-key']][data['entry-key']] = data.value;
}
}
delEntry(json: SettingsUpdate, state: S) {
const data = _.get(json, 'del-entry', false);
if (data) {
delete state.settings[data["bucket-key"]][data["entry-key"]];
delete state.settings[data['bucket-key']][data['entry-key']];
}
}

View File

@ -1,10 +1,9 @@
import React, { ReactNode } from "react";
import React, { ReactNode } from 'react';
import f from 'lodash/fp';
import create, { State } from 'zustand';
import { persist } from 'zustand/middleware';
import produce from 'immer';
import { BackgroundConfig, RemoteContentPolicy, TutorialProgress, tutorialProgress } from "~/types/local-update";
import { BackgroundConfig, RemoteContentPolicy, TutorialProgress, tutorialProgress } from '~/types/local-update';
export interface LocalState extends State {
hideAvatars: boolean;
@ -22,8 +21,8 @@ export interface LocalState extends State {
suspendedFocus?: HTMLElement;
toggleOmnibox: () => void;
set: (fn: (state: LocalState) => void) => void
};
export const selectLocalState =
}
export const selectLocalState =
<K extends keyof LocalState>(keys: K[]) => f.pick<LocalState, K>(keys);
const useLocalState = create<LocalState>(persist((set, get) => ({
@ -33,21 +32,21 @@ const useLocalState = create<LocalState>(persist((set, get) => ({
hideNicknames: false,
tutorialProgress: 'hidden',
tutorialRef: null,
setTutorialRef: (el: HTMLElement | null) => set(produce(state => {
setTutorialRef: (el: HTMLElement | null) => set(produce((state) => {
state.tutorialRef = el;
})),
hideTutorial: () => set(produce(state => {
hideTutorial: () => set(produce((state) => {
state.tutorialProgress = 'hidden';
state.tutorialRef = null;
})),
nextTutStep: () => set(produce(state => {
const currIdx = tutorialProgress.findIndex(p => p === state.tutorialProgress)
nextTutStep: () => set(produce((state) => {
const currIdx = tutorialProgress.findIndex(p => p === state.tutorialProgress);
if(currIdx < tutorialProgress.length) {
state.tutorialProgress = tutorialProgress[currIdx + 1];
}
})),
prevTutStep: () => set(produce(state => {
const currIdx = tutorialProgress.findIndex(p => p === state.tutorialProgress)
prevTutStep: () => set(produce((state) => {
const currIdx = tutorialProgress.findIndex(p => p === state.tutorialProgress);
if(currIdx > 0) {
state.tutorialProgress = tutorialProgress[currIdx - 1];
}
@ -56,11 +55,11 @@ const useLocalState = create<LocalState>(persist((set, get) => ({
imageShown: true,
audioShown: true,
videoShown: true,
oembedShown: true,
oembedShown: true
},
omniboxShown: false,
suspendedFocus: undefined,
toggleOmnibox: () => set(produce(state => {
toggleOmnibox: () => set(produce((state) => {
state.omniboxShown = !state.omniboxShown;
if (typeof state.suspendedFocus?.focus === 'function') {
state.suspendedFocus.focus();
@ -86,7 +85,7 @@ function withLocalState<P, S extends keyof LocalState>(Component: any, stateMemb
(object, key) => ({ ...object, [key]: state[key] }), {}
)
): useLocalState();
return <Component ref={ref} {...localState} {...props} />
return <Component ref={ref} {...localState} {...props} />;
});
}

View File

@ -19,7 +19,7 @@ export default class BaseStore<S extends object> {
clear() {
this.handleEvent({
data: { clear: true },
data: { clear: true }
});
}
@ -30,7 +30,7 @@ export default class BaseStore<S extends object> {
return;
}
if ("clear" in json && json.clear) {
if ('clear' in json && json.clear) {
this.setState(this.initialState());
return;
}

View File

@ -6,7 +6,7 @@ import MetadataReducer from '../reducers/metadata-update';
import LocalReducer from '../reducers/local';
import { StoreState } from './type';
import { Timebox } from '~/types';
import { Timebox } from '@urbit/api';
import { Cage } from '~/types/cage';
import S3Reducer from '../reducers/s3-update';
import { GraphReducer } from '../reducers/graph-update';
@ -16,10 +16,9 @@ import GroupReducer from '../reducers/group-update';
import LaunchReducer from '../reducers/launch-update';
import ConnectionReducer from '../reducers/connection';
import SettingsReducer from '../reducers/settings-update';
import {OrderedMap} from '../lib/OrderedMap';
import { OrderedMap } from '../lib/OrderedMap';
import { BigIntOrderedMap } from '../lib/BigIntOrderedMap';
import {GroupViewReducer} from '../reducers/group-view';
import { GroupViewReducer } from '../reducers/group-view';
export default class GlobalStore extends BaseStore<StoreState> {
inviteReducer = new InviteReducer();
@ -58,7 +57,7 @@ export default class GlobalStore extends BaseStore<StoreState> {
invites: {},
associations: {
groups: {},
graph: {},
graph: {}
},
groups: {},
groupKeys: new Set(),
@ -67,7 +66,7 @@ export default class GlobalStore extends BaseStore<StoreState> {
launch: {
firstTime: false,
tileOrdering: [],
tiles: {},
tiles: {}
},
weather: {},
userLocation: null,
@ -87,7 +86,7 @@ export default class GlobalStore extends BaseStore<StoreState> {
notificationsGraphConfig: {
watchOnSelf: false,
mentions: false,
watching: [],
watching: []
},
unreads: {
graph: {},
@ -95,7 +94,7 @@ export default class GlobalStore extends BaseStore<StoreState> {
},
notificationsCount: 0,
settings: {},
pendingJoin: {},
pendingJoin: {}
};
}

View File

@ -1,20 +1,20 @@
import { Path } from '~/types/noun';
import { Invites } from '~/types/invite-update';
import { Associations } from '~/types/metadata-update';
import { Rolodex } from '~/types/contact-update';
import { Groups } from '~/types/group-update';
import { Path } from '@urbit/api';
import { Invites } from '@urbit/api/invite';
import { Associations } from '@urbit/api/metadata';
import { Rolodex } from '@urbit/api/contacts';
import { Groups } from '@urbit/api/groups';
import { S3State } from '~/types/s3-update';
import { LaunchState, WeatherState } from '~/types/launch-update';
import { ConnectionStatus } from '~/types/connection';
import {Graphs} from '~/types/graph-update';
import { Graphs } from '@urbit/api/graph';
import {
Notifications,
NotificationGraphConfig,
NotificationGraphConfig,
GroupNotificationsConfig,
Unreads,
JoinRequests,
Patp
} from "~/types";
} from '@urbit/api';
export interface StoreState {
// local state
@ -35,7 +35,6 @@ export interface StoreState {
graphs: Graphs;
graphKeys: Set<string>;
// App specific states
// launch state
launch: LaunchState;

View File

@ -1,6 +1,6 @@
import BaseStore from "../store/base";
import BaseApi from "../api/base";
import { Path } from "~/types/noun";
import BaseStore from '../store/base';
import BaseApi from '../api/base';
import { Path } from '@urbit/api';
export default class BaseSubscription<S extends object> {
private errorCount = 0;
@ -19,24 +19,24 @@ export default class BaseSubscription<S extends object> {
// Exists to allow subclasses to hook
restart() {
this.handleEvent({ data: { connection: 'reconnecting' }});
this.handleEvent({ data: { connection: 'reconnecting' } });
this.start();
}
onChannelOpen(e: any) {
this.errorCount = 0;
this.handleEvent({ data: { connection: 'connected' }});
this.handleEvent({ data: { connection: 'connected' } });
}
onChannelError(err) {
console.error('event source error: ', err);
this.errorCount++;
if(this.errorCount >= 5) {
console.error("bailing out, too many retries");
this.handleEvent({ data: { connection: 'disconnected' }});
console.error('bailing out, too many retries');
this.handleEvent({ data: { connection: 'disconnected' } });
return;
}
this.handleEvent({ data: { connection: 'reconnecting' }});
this.handleEvent({ data: { connection: 'reconnecting' } });
setTimeout(() => {
this.restart();
}, Math.pow(2,this.errorCount - 1) * 750);

View File

@ -1,9 +1,8 @@
import BaseSubscription from './base';
import { StoreState } from '../store/type';
import { Path } from '~/types/noun';
import { Path } from '@urbit/api';
import _ from 'lodash';
/**
* Path to subscribe on and app to subscribe to
*/
@ -68,7 +67,7 @@ export default class GlobalSubscription extends BaseSubscription<StoreState> {
}
stopApp(app: AppName) {
this.openSubscriptions[app].map(id => this.unsubscribe(id))
this.openSubscriptions[app].map(id => this.unsubscribe(id));
this.openSubscriptions[app] = [];
}
}

View File

@ -1,21 +1,17 @@
import { ContactUpdate } from "./contact-update";
import { InviteUpdate } from "./invite-update";
import { LocalUpdate } from "./local-update";
import { MetadataUpdate } from "./metadata-update";
import { GroupUpdate } from "./group-update";
import { LaunchUpdate, WeatherState } from "./launch-update";
import { ConnectionStatus } from "./connection";
import { SettingsUpdate } from "./settings";
import { LocalUpdate } from './local-update';
import { LaunchUpdate, WeatherState } from './launch-update';
import { ConnectionStatus } from './connection';
import { ContactUpdate, GroupUpdate, InviteUpdate, MetadataUpdate } from '@urbit/api';
import { SettingsUpdate } from '@urbit/api/settings';
interface MarksToTypes {
readonly json: any;
readonly "contact-update": ContactUpdate;
readonly "invite-update": InviteUpdate;
readonly "metadata-update": MetadataUpdate;
readonly 'contact-update': ContactUpdate;
readonly 'invite-update': InviteUpdate;
readonly 'metadata-update': MetadataUpdate;
readonly groupUpdate: GroupUpdate;
readonly "launch-update": LaunchUpdate;
readonly "link-listen-update": LinkListenUpdate;
readonly "settings-event": SettingsUpdate;
readonly 'launch-update': LaunchUpdate;
readonly 'settings-event': SettingsUpdate;
// not really marks but w/e
readonly 'local': LocalUpdate;
readonly 'weather': WeatherState | {};

View File

@ -1,2 +1 @@
export type ConnectionStatus = 'reconnecting' | 'disconnected' | 'connected';

View File

@ -1,85 +0,0 @@
import { Path, Patp } from "./noun";
export type ContactUpdate =
| ContactUpdateCreate
| ContactUpdateDelete
| ContactUpdateAdd
| ContactUpdateRemove
| ContactUpdateEdit
| ContactUpdateInitial
| ContactUpdateContacts;
interface ContactUpdateCreate {
create: Path;
}
interface ContactUpdateDelete {
delete: Path;
}
interface ContactUpdateAdd {
add: {
path: Path;
ship: Patp;
contact: Contact;
};
}
interface ContactUpdateRemove {
remove: {
path: Path;
ship: Patp;
};
}
interface ContactUpdateEdit {
edit: {
path: Path;
ship: Patp;
"edit-field": ContactEdit;
};
}
interface ContactUpdateInitial {
initial: Rolodex;
}
interface ContactUpdateContacts {
contacts: {
path: Path;
contacts: Contacts;
};
}
//
type ContactAvatar = ContactAvatarUrl | ContactAvatarOcts;
export type Rolodex = {
[p in Path]: Contacts;
};
export type Contacts = {
[p in Patp]: Contact;
};
interface ContactAvatarUrl {
url: string;
}
interface ContactAvatarOcts {
octs: string;
}
export interface Contact {
nickname: string;
email: string;
phone: string;
website: string;
notes: string;
color: string;
avatar: string | null;
}
export type ContactEdit = {
[k in keyof Contact]: Contact[k];
};

View File

@ -1,4 +1,4 @@
import { PatpNoSig } from "./noun";
import { PatpNoSig } from '@urbit/api';
declare global {
interface Window {

View File

@ -1,47 +0,0 @@
import { Patp } from "./noun";
import { BigIntOrderedMap } from "~/logic/lib/BigIntOrderedMap";
export interface TextContent {
text: string;
}
export interface UrlContent {
url: string;
}
export interface CodeContent {
code: {
expresssion: string;
output: string | undefined;
}
}
export interface ReferenceContent {
uid: string;
}
export interface MentionContent {
mention: string;
}
export type Content =
| TextContent
| UrlContent
| CodeContent
| ReferenceContent
| MentionContent;
export interface Post {
author: Patp;
contents: Content[];
hash: string | null;
index: string;
pending?: boolean;
signatures: string[];
"time-sent": number;
}
export interface GraphNode {
children: Graph;
post: Post;
}
export type Graph = BigIntOrderedMap<GraphNode>;
export type Graphs = { [rid: string]: Graph };

View File

@ -1,180 +0,0 @@
import { PatpNoSig, Path, Jug, ShipRank, Enc } from './noun';
export const roleTags = ['janitor', 'moderator', 'admin'] as const;
export type RoleTags = typeof roleTags[number];
interface RoleTag {
tag: 'admin' | 'moderator' | 'janitor';
}
interface AppTag {
app: string;
resource: string;
tag: string;
}
export type Tag = AppTag | RoleTag;
export interface InvitePolicy {
invite: {
pending: Set<PatpNoSig>;
};
}
export interface OpenPolicy {
open: {
banned: Set<PatpNoSig>;
banRanks: Set<ShipRank>;
};
}
export interface Resource {
name: string;
ship: PatpNoSig;
}
export type OpenPolicyDiff =
| AllowRanksDiff
| BanRanksDiff
| AllowShipsDiff
| BanShipsDiff;
interface AllowRanksDiff {
allowRanks: ShipRank[];
}
interface BanRanksDiff {
banRanks: ShipRank[];
}
interface AllowShipsDiff {
allowShips: PatpNoSig[];
}
interface BanShipsDiff {
banShips: PatpNoSig[];
}
export type InvitePolicyDiff = AddInvitesDiff | RemoveInvitesDiff;
interface AddInvitesDiff {
addInvites: PatpNoSig[];
}
interface RemoveInvitesDiff {
removeInvites: PatpNoSig[];
}
interface ReplacePolicyDiff {
replace: GroupPolicy;
}
export type GroupPolicyDiff =
| { open: OpenPolicyDiff }
| { invite: InvitePolicyDiff }
| ReplacePolicyDiff;
export type GroupPolicy = OpenPolicy | InvitePolicy;
interface TaggedShips {
[tag: string]: Set<PatpNoSig>;
}
export interface Tags {
role: TaggedShips;
[app: string]: TaggedShips;
}
export interface Group {
members: Set<PatpNoSig>;
tags: Tags;
policy: GroupPolicy;
hidden: boolean;
}
export type Groups = {
[p in Path]: Group;
};
interface GroupUpdateInitial {
initial: Enc<Groups>;
}
interface GroupUpdateAddGroup {
addGroup: {
resource: Resource;
policy: Enc<GroupPolicy>;
hidden: boolean;
};
}
interface GroupUpdateAddMembers {
addMembers: {
ships: PatpNoSig[];
resource: Resource;
};
}
interface GroupUpdateRemoveMembers {
removeMembers: {
ships: PatpNoSig[];
resource: Resource;
};
}
interface GroupUpdateAddTag {
addTag: {
tag: Tag;
resource: Resource;
ships: PatpNoSig[];
};
}
interface GroupUpdateRemoveTag {
removeTag: {
tag: Tag;
resource: Resource;
ships: PatpNoSig;
};
}
interface GroupUpdateChangePolicy {
changePolicy: { resource: Resource; diff: GroupPolicyDiff };
}
interface GroupUpdateRemoveGroup {
removeGroup: {
resource: Resource;
};
}
interface GroupUpdateExpose {
expose: {
resource: Resource;
};
}
interface GroupUpdateInitialGroup {
initialGroup: {
resource: Resource;
group: Enc<Group>;
};
}
export type GroupUpdate =
| GroupUpdateInitial
| GroupUpdateAddGroup
| GroupUpdateAddMembers
| GroupUpdateRemoveMembers
| GroupUpdateAddTag
| GroupUpdateRemoveTag
| GroupUpdateChangePolicy
| GroupUpdateRemoveGroup
| GroupUpdateExpose
| GroupUpdateInitialGroup;
export type GroupAction = Omit<GroupUpdate, 'initialGroup' | 'initial'>;
export const groupBunts = {
group: (): Group => ({ members: new Set(), tags: { role: {} }, hidden: false, policy: groupBunts.policy() }),
policy: (): GroupPolicy => ({ open: { banned: new Set(), banRanks: new Set() } })
};

View File

@ -1,11 +0,0 @@
export const joinError = ['no-perms', 'strange'] as const;
export type JoinError = typeof joinError[number];
export const joinResult = ['done', ...joinError] as const;
export type JoinResult = typeof joinResult[number];
export const joinProgress = ['start', 'added', ...joinResult] as const;
export type JoinProgress = typeof joinProgress[number];
export interface JoinRequests {
[rid: string]: JoinProgress;
}

View File

@ -1,68 +0,0 @@
import _ from "lodash";
import { Post } from "./graph-update";
import { GroupUpdate } from "./group-update";
import { BigIntOrderedMap } from "~/logic/lib/BigIntOrderedMap";
export type GraphNotifDescription = "link" | "comment" | "note" | "mention";
export interface UnreadStats {
unreads: Set<string> | number;
notifications: number;
last: number;
}
export interface GraphNotifIndex {
graph: string;
group: string;
description: GraphNotifDescription;
module: string;
index: string;
}
export interface GroupNotifIndex {
group: string;
description: string;
}
export type NotifIndex =
| { graph: GraphNotifIndex }
| { group: GroupNotifIndex };
export type GraphNotificationContents = Post[];
export type GroupNotificationContents = GroupUpdate[];
export type NotificationContents =
| { graph: GraphNotificationContents }
| { group: GroupNotificationContents };
export interface Notification {
read: boolean;
time: number;
contents: NotificationContents;
}
export interface IndexedNotification {
index: NotifIndex;
notification: Notification;
}
export type Timebox = IndexedNotification[];
export type Notifications = BigIntOrderedMap<Timebox>;
export interface NotificationGraphConfig {
watchOnSelf: boolean;
mentions: boolean;
watching: WatchedIndex[]
}
export interface Unreads {
graph: Record<string, Record<string, UnreadStats>>;
group: Record<string, UnreadStats>;
}
interface WatchedIndex {
graph: string;
index: string;
}
export type GroupNotificationsConfig = string[];

View File

@ -1,16 +1,8 @@
export * from './cage';
export * from './connection';
export * from './contact-update';
export * from './global';
export * from './group-update';
export * from './group-view';
export * from './graph-update';
export * from './hark-update';
export * from './invite-update';
export * from './launch-update';
export * from './local-update';
export * from './metadata-update';
export * from './noun';
export * from './s3-update';
export * from './workspace';
export * from './util';

View File

@ -1,68 +0,0 @@
import { Serial, PatpNoSig, Path } from './noun';
import {Resource} from './group-update';
export type InviteUpdate =
InviteUpdateInitial
| InviteUpdateCreate
| InviteUpdateDelete
| InviteUpdateInvite
| InviteUpdateAccepted
| InviteUpdateDecline;
interface InviteUpdateInitial {
initial: Invites;
}
interface InviteUpdateCreate {
create: {
path: Path;
};
}
interface InviteUpdateDelete {
delete: {
path: Path;
};
}
interface InviteUpdateInvite {
invite: {
path: Path;
uid: Serial;
invite: Invite;
};
}
interface InviteUpdateAccepted {
accepted: {
path: Path;
uid: Serial;
};
}
interface InviteUpdateDecline {
decline: {
path: Path;
uid: Serial;
};
}
// actual datastructures
export type Invites = {
[p in Path]: AppInvites;
};
export type AppInvites = {
[s in Serial]: Invite;
};
export interface Invite {
app: string;
recipient: PatpNoSig;
resource: Resource;
ship: PatpNoSig;
text: string;
}

View File

@ -1,11 +1,9 @@
export type LaunchUpdate =
LaunchUpdateInitial
| LaunchUpdateFirstTime
| LaunchUpdateOrder
| LaunchUpdateIsShown;
interface LaunchUpdateInitial {
initial: LaunchState;
}

View File

@ -1,68 +0,0 @@
import { AppName, Path, Patp } from './noun';
export type MetadataUpdate =
MetadataUpdateInitial
| MetadataUpdateAdd
| MetadataUpdateUpdate
| MetadataUpdateRemove;
interface MetadataUpdateInitial {
associations: ResourceAssociations;
}
type ResourceAssociations = {
[p in Path]: Association;
}
type MetadataUpdateAdd = {
add: Association;
}
type MetadataUpdateUpdate = {
update: Association;
}
type MetadataUpdateRemove = {
remove: Resource & {
group: Path;
}
}
export interface MetadataUpdatePreview {
group: string;
channels: Associations;
"channel-count": number;
members: number;
metadata: Metadata;
}
export type Associations = Record<AppName, AppAssociations>;
export type AppAssociations = {
[p in Path]: Association;
}
interface Resource {
resource: Path;
'app-name': AppName;
}
export type Association = Resource & {
group: Path;
metadata: Metadata;
};
export interface Metadata {
color: string;
creator: Patp;
'date-created': string;
description: string;
title: string;
module: string;
picture: string;
preview: boolean;
vip: PermVariation;
}
export type PermVariation = '' | 'reader-comments' | 'member-metadata';

View File

@ -1,49 +0,0 @@
/**
* Martian embassy
*/
// an urbit style path rendered as string
export type Path = string;
// patp including leading sig
export type Patp = string;
// patp excluding leading sig
export type PatpNoSig = string;
// @uvH encoded string
export type Serial = string;
// jug from hoon
export type Jug<K,V> = Map<K,Set<V>>;
// name of app
export type AppName = 'contacts' | 'groups' | 'graph';
export function getTagFromFrond<O>(frond: O): keyof O {
const tags = Object.keys(frond) as Array<keyof O>;
const tag = tags[0];
if(!tag) {
throw new Error("bad frond");
}
return tag;
}
export type ShipRank = 'czar' | 'king' | 'duke' | 'earl' | 'pawn';
export type SetElement<S> = S extends Set<(infer T)> ? T : never;
export type MapKey<M> = M extends Map<(infer K), any> ? K : never;
export type MapValue<M> = M extends Map<any, (infer V)> ? V : never;
/**
* Turns sets into arrays and maps into objects so we can send them over the wire
*/
export type Enc<S> =
S extends Set<any> ?
Enc<SetElement<S>>[] :
S extends Map<string, any> ?
{ [s: string]: Enc<MapValue<S>> } :
S extends object ?
{ [K in keyof S]: Enc<S[K]> } :
S;

View File

@ -1,6 +1,4 @@
export interface S3Credentials {
export interface S3Credentials {
endpoint: string;
accessKeyId: string;
secretAccessKey: string;
@ -51,7 +49,6 @@ interface S3UpdateSecretAccessKey {
setSecretAccessKey: string;
}
export type S3Update =
S3UpdateCredentials
| S3UpdateConfiguration

View File

@ -1,55 +0,0 @@
export type Key = string;
export type Value = string | boolean | number;
export type Bucket = Map<string, Value>;
export type Settings = Map<string, Bucket>;
interface PutBucket {
"put-bucket": {
"bucket-key": Key;
"bucket": Bucket;
};
}
interface DelBucket {
"del-bucket": {
"bucket-key": Key;
};
}
interface PutEntry {
"put-entry": {
"bucket-key": Key;
"entry-key": Key;
"value": Value;
};
}
interface DelEntry {
"del-entry": {
"bucket-key": Key;
"entry-key": Key;
};
}
interface AllData {
"all": Settings;
}
interface BucketData {
"bucket": Bucket;
}
interface EntryData {
"entry": Value;
}
export type SettingsUpdate =
| PutBucket
| DelBucket
| PutEntry
| DelEntry;
export type SettingsData =
| AllData
| BucketData
| EntryData;

View File

@ -1,4 +1,4 @@
import { Icon } from "@tlon/indigo-react";
import { Icon } from '@tlon/indigo-react';
export type PropFunc<T extends (...args: any[]) => any> = Parameters<T>[0];
export type Primitive = string | number | undefined | symbol | null | boolean;

View File

@ -1,5 +1,3 @@
interface GroupWorkspace {
type: 'group';
group: string;

View File

@ -3,7 +3,7 @@ import { RouteComponentProps } from 'react-router-dom';
import { Col } from '@tlon/indigo-react';
import _ from 'lodash';
import { Association } from '~/types/metadata-update';
import { Association } from '@urbit/api/metadata';
import { StoreState } from '~/logic/store/type';
import { useFileDrag } from '~/logic/lib/useDrag';
import ChatWindow from './components/ChatWindow';
@ -86,12 +86,16 @@ export function ChatResource(props: ChatResourceProps) {
useEffect(() => {
(async () => {
if (!res) { return; }
if (!group) { return; }
if (!res) {
return;
}
if (!group) {
return;
}
if (group.hidden) {
const members = _.compact(await Promise.all(
Array.from(group.members)
.map(s => {
.map((s) => {
const ship = `~${s}`;
if(s === window.ship) {
return Promise.resolve(null);
@ -101,7 +105,7 @@ export function ChatResource(props: ChatResourceProps) {
'personal',
ship,
true
).then(isAllowed => {
).then((isAllowed) => {
return isAllowed ? null : ship;
});
})
@ -113,7 +117,6 @@ export function ChatResource(props: ChatResourceProps) {
} else {
setShowBanner(false);
}
} else {
const groupShared = await props.api.contacts.fetchIsAllowed(
`~${window.ship}`,
@ -124,14 +127,13 @@ export function ChatResource(props: ChatResourceProps) {
setShowBanner(!groupShared);
}
})();
}, [groupPath]);
if(!graph) {
return <Loading />;
}
var modifiedContacts = { ...contacts };
const modifiedContacts = { ...contacts };
delete modifiedContacts[`~${window.ship}`];
return (
@ -145,7 +147,7 @@ export function ChatResource(props: ChatResourceProps) {
setShowBanner={setShowBanner}
group={group}
groupPath={groupPath}
/>
/>
{dragging && <SubmitDragger />}
<ChatWindow
history={props.history}

View File

@ -7,7 +7,7 @@ import { createPost } from '~/logic/api/graph';
import tokenizeMessage, { isUrl } from '~/logic/lib/tokenizeMessage';
import GlobalApi from '~/logic/api/global';
import { Envelope } from '~/types/chat-update';
import { Contacts, Content } from '~/types';
import { Contacts, Content } from '@urbit/api';
import { Row, BaseImage, Box, Icon, LoadingSpinner } from '@tlon/indigo-react';
import withS3 from '~/views/components/withS3';
import { withLocalState } from '~/logic/state/local';
@ -15,12 +15,12 @@ import { withLocalState } from '~/logic/state/local';
type ChatInputProps = IuseS3 & {
api: GlobalApi;
numMsgs: number;
station: any;
ourContact: any;
station: unknown;
ourContact: unknown;
envelopes: Envelope[];
contacts: Contacts;
onUnmount(msg: string): void;
s3: any;
s3: unknown;
placeholder: string;
message: string;
deleteMessage(): void;
@ -67,14 +67,14 @@ 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);
});
return;
}
const post = createPost(tokenizeMessage((text)))
const post = createPost(tokenizeMessage((text)));
props.deleteMessage();
@ -110,7 +110,7 @@ class ChatInput extends Component<ChatInputProps, ChatInputState> {
if (!this.props.canUpload) {
return;
}
Array.from(files).forEach(file => {
Array.from(files).forEach((file) => {
this.props.uploadDefault(file)
.then(this.uploadSuccess)
.catch(this.uploadError);
@ -178,7 +178,7 @@ class ChatInput extends Component<ChatInputProps, ChatInputState> {
width="16"
height="16"
onClick={() => this.props.promptUpload().then(this.uploadSuccess)}
/>
/>
: null
}
</Box>
@ -200,4 +200,4 @@ class ChatInput extends Component<ChatInputProps, ChatInputState> {
}
}
export default withLocalState(withS3(ChatInput, {accept: 'image/*'}), ['hideAvatars']);
export default withLocalState(withS3(ChatInput, { accept: 'image/*' }), ['hideAvatars']);

View File

@ -17,7 +17,7 @@ import {
useShowNickname,
useHovering
} from '~/logic/lib/util';
import { Group, Association, Contacts, Post, Groups, Associations } from '~/types';
import { Group, Association, Contacts, Post, Groups, Associations } from '@urbit/api';
import TextContent from './content/text';
import CodeContent from './content/code';
import RemoteContent from '~/views/components/RemoteContent';
@ -66,12 +66,12 @@ interface ChatMessageProps {
contacts: Contacts;
className?: string;
isPending: boolean;
style?: any;
style?: unknown;
scrollWindow: HTMLDivElement;
isLastMessage?: boolean;
unreadMarkerRef: React.RefObject<HTMLDivElement>;
history: any;
api: any;
history: unknown;
api: GlobalApi;
highlighted?: boolean;
}
@ -149,7 +149,7 @@ export default class ChatMessage extends Component<ChatMessageProps> {
highlighted,
fontSize,
associations,
groups,
groups
};
const unreadContainerStyle = {
@ -230,7 +230,7 @@ export const MessageWithSigil = (props) => {
fontSize
} = props;
const dark = useLocalState((state) => state.dark);
const dark = useLocalState(state => state.dark);
const datestamp = moment
.unix(msg['time-sent'] / 1000)
@ -255,7 +255,7 @@ export const MessageWithSigil = (props) => {
const [showOverlay, setShowOverlay] = useState(false);
const toggleOverlay = () => {
setShowOverlay((value) => !value);
setShowOverlay(value => !value);
};
const showCopyNotice = () => {

View File

@ -1,22 +1,17 @@
import React, { Component } from "react";
import { RouteComponentProps } from "react-router-dom";
import _ from "lodash";
import React, { Component } from 'react';
import { RouteComponentProps } from 'react-router-dom';
import _ from 'lodash';
import bigInt, { BigInteger } from 'big-integer';
import { Col } from '@tlon/indigo-react';
import { Patp, Contacts, Association, Associations, Group, Groups, Graph } from '@urbit/api';
import GlobalApi from "~/logic/api/global";
import { Patp, Path } from "~/types/noun";
import { Contacts } from "~/types/contact-update";
import { Association, Associations } from "~/types/metadata-update";
import { Group, Groups } from "~/types/group-update";
import { Envelope, IMessage } from "~/types/chat-update";
import { Graph } from "~/types";
import GlobalApi from '~/logic/api/global';
import VirtualScroller from "~/views/components/VirtualScroller";
import VirtualScroller from '~/views/components/VirtualScroller';
import ChatMessage, { MessagePlaceholder } from './ChatMessage';
import { UnreadNotice } from "./unread-notice";
import { UnreadNotice } from './unread-notice';
const INITIAL_LOAD = 20;
const DEFAULT_BACKLOG_SIZE = 100;
@ -66,8 +61,6 @@ export default class ChatWindow extends Component<ChatWindowProps, ChatWindowSta
unreadIndex: bigInt.zero
};
this.dismissUnread = this.dismissUnread.bind(this);
this.scrollToUnread = this.scrollToUnread.bind(this);
this.handleWindowBlur = this.handleWindowBlur.bind(this);
@ -110,7 +103,7 @@ export default class ChatWindow extends Component<ChatWindowProps, ChatWindowSta
}
this.setState({
unreadIndex
})
});
}
handleWindowBlur() {
@ -135,7 +128,6 @@ export default class ChatWindow extends Component<ChatWindowProps, ChatWindowSta
this.calculateUnreadIndex();
}
if(this.prevSize !== graph.size) {
if(this.state.unreadIndex.eq(bigInt.zero)) {
this.calculateUnreadIndex();
@ -170,8 +162,10 @@ export default class ChatWindow extends Component<ChatWindowProps, ChatWindowSta
dismissUnread() {
const { association } = this.props;
if (this.state.fetchPending) return;
if (this.props.unreadCount === 0) return;
if (this.state.fetchPending)
return;
if (this.props.unreadCount === 0)
return;
this.props.api.hark.markCountAsRead(association, '/', 'message');
this.props.api.hark.markCountAsRead(association, '/', 'mention');
}
@ -189,14 +183,14 @@ export default class ChatWindow extends Component<ChatWindowProps, ChatWindowSta
const currSize = graph.size;
if(newer && !this.loadedNewest) {
const [index] = graph.peekLargest()!;
await api.graph.getYoungerSiblings(ship,name, 20, `/${index.toString()}`)
await api.graph.getYoungerSiblings(ship,name, 20, `/${index.toString()}`);
if(currSize === graph.size) {
console.log('loaded all newest');
this.loadedNewest = true;
}
} else if(!newer && !this.loadedOldest) {
const [index] = graph.peekSmallest()!;
await api.graph.getOlderSiblings(ship,name, 20, `/${index.toString()}`)
await api.graph.getOlderSiblings(ship,name, 20, `/${index.toString()}`);
this.calculateUnreadIndex();
if(currSize === graph.size) {
console.log('loaded all oldest');
@ -204,7 +198,6 @@ export default class ChatWindow extends Component<ChatWindowProps, ChatWindowSta
}
}
this.setState({ fetchPending: false });
}
onScroll({ scrollTop, scrollHeight, windowHeight }) {
@ -216,10 +209,13 @@ export default class ChatWindow extends Component<ChatWindowProps, ChatWindowSta
}
dismissIfLineVisible() {
if (this.props.unreadCount === 0) return;
if (!this.unreadMarkerRef.current || !this.virtualList?.window) return;
if (this.props.unreadCount === 0)
return;
if (!this.unreadMarkerRef.current || !this.virtualList?.window)
return;
const parent = this.unreadMarkerRef.current.parentElement?.parentElement;
if (!parent) return;
if (!parent)
return;
const { scrollTop, scrollHeight, offsetHeight } = this.virtualList.window;
if (
(scrollHeight - parent.offsetTop > scrollTop)
@ -246,7 +242,6 @@ export default class ChatWindow extends Component<ChatWindowProps, ChatWindowSta
const unreadMarkerRef = this.unreadMarkerRef;
const messageProps = { association, group, contacts, unreadMarkerRef, history, api, groups, associations };
const keys = graph.keys().reverse();
@ -262,7 +257,9 @@ export default class ChatWindow extends Component<ChatWindowProps, ChatWindowSta
onClick={this.scrollToUnread}
/>
<VirtualScroller
ref={list => {this.virtualList = list}}
ref={(list) => {
this.virtualList = list;
}}
origin="bottom"
style={{ height: '100%' }}
onStartReached={() => {
@ -274,7 +271,8 @@ export default class ChatWindow extends Component<ChatWindowProps, ChatWindowSta
size={graph.size}
renderer={({ index, measure, scrollWindow }) => {
const msg = graph.get(index)?.post;
if (!msg) return null;
if (!msg)
return null;
if (!this.state.initialized) {
return <MessagePlaceholder key={index.toString()} height="64px" index={index} />;
}
@ -285,7 +283,6 @@ export default class ChatWindow extends Component<ChatWindowProps, ChatWindowSta
const prevIdx = keys[graphIdx+1];
const nextIdx = keys[graphIdx-1];
const isLastRead: boolean = this.state.unreadIndex.eq(index);
const props = { measure, highlighted, scrollWindow, isPending, isLastRead, isLastMessage, msg, ...messageProps };
return (

View File

@ -1,14 +1,14 @@
import React, {useRef} from "react";
import { Box, Text, Col } from "@tlon/indigo-react";
import f from "lodash/fp";
import _ from "lodash";
import React, { useRef } from 'react';
import { Box, Text, Col } from '@tlon/indigo-react';
import f from 'lodash/fp';
import _ from 'lodash';
import { Associations, Association, Unreads, UnreadStats } from "~/types";
import { alphabeticalOrder } from "~/logic/lib/util";
import { getUnreadCount, getNotificationCount } from "~/logic/lib/hark";
import Tile from "../components/tiles/tile";
import { useTutorialModal } from "~/views/components/useTutorialModal";
import {TUTORIAL_HOST, TUTORIAL_GROUP} from "~/logic/lib/tutorialModal";
import { Associations, Association, Unreads, UnreadStats } from '@urbit/api';
import { alphabeticalOrder } from '~/logic/lib/util';
import { getUnreadCount, getNotificationCount } from '~/logic/lib/hark';
import Tile from '../components/tiles/tile';
import { useTutorialModal } from '~/views/components/useTutorialModal';
import { TUTORIAL_HOST, TUTORIAL_GROUP } from '~/logic/lib/tutorialModal';
interface GroupsProps {
associations: Associations;
@ -17,7 +17,6 @@ interface GroupsProps {
const sortGroupsAlph = (a: Association, b: Association) =>
alphabeticalOrder(a.metadata.title, b.metadata.title);
const getGraphUnreads = (associations: Associations, unreads: Unreads) => (path: string) =>
f.flow(
f.pickBy((a: Association) => a.group === path),
@ -34,12 +33,11 @@ const getGraphNotifications = (associations: Associations, unreads: Unreads) =>
f.reduce(f.add, 0)
)(associations.graph);
export default function Groups(props: GroupsProps & Parameters<typeof Box>[0]) {
const { associations, unreads, inbox, ...boxProps } = props;
const groups = Object.values(associations?.groups || {})
.filter((e) => e?.group in props.groups)
.filter(e => e?.group in props.groups)
.sort(sortGroupsAlph);
const graphUnreads = getGraphUnreads(associations || {}, unreads);
const graphNotifications = getGraphNotifications(associations || {}, unreads);
@ -48,7 +46,7 @@ export default function Groups(props: GroupsProps & Parameters<typeof Box>[0]) {
<>
{groups.map((group, index) => {
const path = group?.group;
const unreadCount = graphUnreads(path)
const unreadCount = graphUnreads(path);
const notCount = graphNotifications(path);
return (

View File

@ -1,6 +1,6 @@
import React from "react"
import { Box, Button, Icon, Text } from "@tlon/indigo-react"
import {useModal} from "~/logic/lib/useModal";
import React from 'react';
import { Box, Button, Icon, Text } from '@tlon/indigo-react';
import { useModal } from '~/logic/lib/useModal';
const ModalButton = (props) => {
const {
@ -13,7 +13,6 @@ const ModalButton = (props) => {
} = props;
const { modal, showModal } = useModal({ modal: props.children });
return (
<>
{modal}
@ -33,6 +32,6 @@ const ModalButton = (props) => {
</Button>
</>
);
}
};
export default ModalButton;

View File

@ -1,19 +1,18 @@
import React, { useEffect, useCallback } from "react";
import { Box, Row, Col, Center, LoadingSpinner, Text } from "@tlon/indigo-react";
import { Switch, Route, Link } from "react-router-dom";
import React, { useEffect } from 'react';
import { Box, Col, Center, LoadingSpinner, Text } from '@tlon/indigo-react';
import { Switch, Route, Link } from 'react-router-dom';
import bigInt from 'big-integer';
import GlobalApi from "~/logic/api/global";
import { StoreState } from "~/logic/store/type";
import { uxToHex } from '~/logic/lib/util';
import { RouteComponentProps } from "react-router-dom";
import GlobalApi from '~/logic/api/global';
import { StoreState } from '~/logic/store/type';
import { RouteComponentProps } from 'react-router-dom';
import { LinkItem } from "./components/LinkItem";
import { LinkPreview } from "./components/link-preview";
import { LinkWindow } from "./LinkWindow";
import { Comments } from "~/views/components/Comments";
import { LinkItem } from './components/LinkItem';
import { LinkWindow } from './LinkWindow';
import { Comments } from '~/views/components/Comments';
import "./css/custom.css";
import './css/custom.css';
import { Association } from '@urbit/api/metadata';
const emptyMeasure = () => {};
@ -38,11 +37,11 @@ export function LinkResource(props: LinkResourceProps) {
history
} = props;
const rid = association.resource;
const rid = association.resource;
const relativePath = (p: string) => `${baseUrl}/resource/link${rid}${p}`;
const [, , ship, name] = rid.split("/");
const [, , ship, name] = rid.split('/');
const resourcePath = `${ship.slice(1)}/${name}`;
const resource = associations.graph[rid]
? associations.graph[rid]
@ -58,7 +57,7 @@ export function LinkResource(props: LinkResourceProps) {
const resourceUrl = `${baseUrl}/resource/link${rid}`;
if (!graph) {
return <Center width='100%' height='100%'><LoadingSpinner/></Center>;
return <Center width='100%' height='100%'><LoadingSpinner /></Center>;
}
return (
@ -66,7 +65,7 @@ export function LinkResource(props: LinkResourceProps) {
<Switch>
<Route
exact
path={relativePath("")}
path={relativePath('')}
render={(props) => {
return (
<LinkWindow
@ -86,7 +85,7 @@ export function LinkResource(props: LinkResourceProps) {
}}
/>
<Route
path={relativePath("/:index(\\d+)/:commentId?")}
path={relativePath('/:index(\\d+)/:commentId?')}
render={(props) => {
const index = bigInt(props.match.params.index);
const editCommentId = props.match.params.commentId || null;
@ -95,7 +94,7 @@ export function LinkResource(props: LinkResourceProps) {
return <div>Malformed URL</div>;
}
const node = !!graph ? graph.get(index) : null;
const node = graph ? graph.get(index) : null;
if (!node) {
return <Box>Not found</Box>;
@ -106,7 +105,7 @@ export function LinkResource(props: LinkResourceProps) {
return (
<Col alignItems="center" overflowY="auto" width="100%">
<Col width="100%" p={3} maxWidth="768px">
<Link to={resourceUrl}><Text px={3} bold>{"<- Back"}</Text></Link>
<Link to={resourceUrl}><Text px={3} bold>{'<- Back'}</Text></Link>
<LinkItem
contacts={contacts}
key={node.post.index}

View File

@ -1,5 +1,5 @@
import React, { useRef, useCallback, useEffect, useMemo } from "react";
import { Col } from "@tlon/indigo-react";
import React, { useRef, useCallback, useEffect, useMemo } from 'react';
import { Col } from '@tlon/indigo-react';
import bigInt from 'big-integer';
import {
Association,
@ -9,13 +9,13 @@ import {
LocalUpdateRemoteContentPolicy,
Group,
Rolodex,
S3State,
} from "~/types";
import GlobalApi from "~/logic/api/global";
import VirtualScroller from "~/views/components/VirtualScroller";
import { LinkItem } from "./components/LinkItem";
import LinkSubmit from "./components/LinkSubmit";
import {isWriter} from "~/logic/lib/group";
S3State
} from '@urbit/api';
import GlobalApi from '~/logic/api/global';
import VirtualScroller from '~/views/components/VirtualScroller';
import { LinkItem } from './components/LinkItem';
import LinkSubmit from './components/LinkSubmit';
import { isWriter } from '~/logic/lib/group';
interface LinkWindowProps {
association: Association;
@ -44,18 +44,19 @@ export function LinkWindow(props: LinkWindowProps) {
useEffect(() => {
const list = virtualList?.current;
if(!list) return;
if(!list)
return;
list.calculateVisibleItems();
}, [graph.size]);
const first = graph.peekLargest()?.[0];
const [,,ship, name] = association.resource.split('/');
const canWrite = isWriter(props.group, association.resource)
const canWrite = isWriter(props.group, association.resource);
const style = useMemo(() =>
({
height: "100%",
width: "100%",
height: '100%',
width: '100%',
display: 'flex',
flexDirection: 'column',
alignItems: 'center'
@ -76,7 +77,7 @@ export function LinkWindow(props: LinkWindowProps) {
return (
<VirtualScroller
ref={(l) => (virtualList.current = l ?? undefined)}
ref={l => (virtualList.current = l ?? undefined)}
origin="top"
style={style}
onStartReached={() => {}}
@ -86,11 +87,12 @@ export function LinkWindow(props: LinkWindowProps) {
renderer={({ index, measure, scrollWindow }) => {
const node = graph.get(index);
const post = node?.post;
if (!node || !post) return null;
if (!node || !post)
return null;
const linkProps = {
...props,
node,
measure,
measure
};
if(canWrite && index.eq(first ?? bigInt.zero)) {
return (
@ -100,7 +102,7 @@ export function LinkWindow(props: LinkWindowProps) {
</Col>
<LinkItem {...linkProps} />
</React.Fragment>
)
);
}
return <LinkItem key={index.toString()} {...linkProps} />;
}}

View File

@ -1,12 +1,12 @@
import React, { useState, useEffect, useRef, useCallback } from 'react';
import React, { useState, useEffect, useRef, useCallback, ReactElement } from 'react';
import { Link } from 'react-router-dom';
import { Row, Col, Anchor, Box, Text, Icon, Action } from '@tlon/indigo-react';
import { GraphNode, Group, Rolodex, Unreads } from '@urbit/api';
import { writeText } from '~/logic/lib/util';
import Author from '~/views/components/Author';
import { roleForShip } from '~/logic/lib/group';
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';
@ -22,7 +22,7 @@ interface LinkItemProps {
measure: (el: any) => void;
}
export const LinkItem = (props: LinkItemProps) => {
export const LinkItem = (props: LinkItemProps): ReactElement => {
const {
node,
resource,
@ -46,7 +46,7 @@ export const LinkItem = (props: LinkItemProps) => {
// FF will only update on next tick
setTimeout(() => {
console.log(remoteRef.current);
if(document.activeElement instanceof HTMLIFrameElement
if(document.activeElement instanceof HTMLIFrameElement
&& remoteRef?.current?.containerRef?.contains(document.activeElement)) {
markRead();
}
@ -55,8 +55,7 @@ export const LinkItem = (props: LinkItemProps) => {
window.addEventListener('blur', onBlur);
return () => {
window.removeEventListener('blur', onBlur);
}
};
}, [markRead]);
const URLparser = new RegExp(
@ -94,11 +93,9 @@ export const LinkItem = (props: LinkItemProps) => {
const commColor = (props.unreads.graph?.[appPath]?.[`/${index}`]?.unreads ?? 0) > 0 ? 'blue' : 'gray';
const isUnread = props.unreads.graph?.[appPath]?.['/']?.unreads?.has(node.post.index);
const onMeasure = useCallback(() => {
ref.current && measure(ref.current);
}, [ref.current, measure])
}, [ref.current, measure]);
useEffect(() => {
onMeasure();
@ -120,7 +117,9 @@ export const LinkItem = (props: LinkItemProps) => {
onClick={markRead}
>
<RemoteContent
ref={r => { remoteRef.current = r}}
ref={(r) => {
remoteRef.current = r;
}}
url={contents[1].url}
text={contents[0].text}
unfold={true}
@ -143,7 +142,8 @@ export const LinkItem = (props: LinkItemProps) => {
alignSelf: 'center',
style: { textOverflow: 'ellipsis', whiteSpace: 'pre', width: '100%' },
p: 2
}} />
}}
/>
<Text color="gray" p={2} flexShrink={0}>
<Anchor target="_blank" rel="noopener noreferrer" style={{ textDecoration: 'none' }} href={contents[1].url}>
<Box display='flex'>
@ -189,7 +189,7 @@ export const LinkItem = (props: LinkItemProps) => {
}
</Col>
}
>
>
<Icon ml="2" display="block" icon="Ellipsis" color="gray" />
</Dropdown>

View File

@ -1,22 +1,22 @@
import { BaseInput, Box, Button, LoadingSpinner, Text } from "@tlon/indigo-react";
import React, { useCallback, useState } from "react";
import GlobalApi from "~/logic/api/global";
import { useFileDrag } from "~/logic/lib/useDrag";
import useS3 from "~/logic/lib/useS3";
import { S3State } from "~/types";
import SubmitDragger from "~/views/components/SubmitDragger";
import { createPost } from "~/logic/api/graph";
import { hasProvider } from "oembed-parser";
import { BaseInput, Box, Button, LoadingSpinner, Text } from '@tlon/indigo-react';
import React, { useCallback, useState } from 'react';
import GlobalApi from '~/logic/api/global';
import { useFileDrag } from '~/logic/lib/useDrag';
import useS3 from '~/logic/lib/useS3';
import { S3State } from '@urbit/api';
import SubmitDragger from '~/views/components/SubmitDragger';
import { createPost } from '~/logic/api/graph';
import { hasProvider } from 'oembed-parser';
interface LinkSubmitProps {
api: GlobalApi;
s3: S3State;
name: string;
ship: string;
};
}
const LinkSubmit = (props: LinkSubmitProps) => {
let { canUpload, uploadDefault, uploading, promptUpload } = useS3(props.s3);
const { canUpload, uploadDefault, uploading, promptUpload } = useS3(props.s3);
const [submitFocused, setSubmitFocused] = useState(false);
const [urlFocused, setUrlFocused] = useState(false);
@ -100,7 +100,7 @@ const LinkSubmit = (props: LinkSubmitProps) => {
const onLinkChange = (linkValue: string) => {
setLinkValueHook(linkValue);
const link = validateLink(linkValue)
const link = validateLink(linkValue);
setLinkValid(link);
};
@ -133,7 +133,7 @@ const LinkSubmit = (props: LinkSubmitProps) => {
px={2}
pt={2}
style={{ pointerEvents: 'none' }}
>{canUpload
>{canUpload
? <>
Drop or{' '}
<Text
@ -169,7 +169,7 @@ const LinkSubmit = (props: LinkSubmitProps) => {
zIndex={9}
alignItems="center"
justifyContent="center"
>
>
<LoadingSpinner />
</Box>}
{dragging && <SubmitDragger />}
@ -223,4 +223,4 @@ const LinkSubmit = (props: LinkSubmitProps) => {
);
};
export default LinkSubmit;
export default LinkSubmit;

View File

@ -1,31 +1,29 @@
import React, { ReactNode, useCallback } from "react";
import moment from "moment";
import { Row, Box, Col, Text, Anchor, Icon, Action } from "@tlon/indigo-react";
import { Link, useHistory } from "react-router-dom";
import _ from "lodash";
import React, { ReactElement, useCallback } from 'react';
import moment from 'moment';
import _ from 'lodash';
import { useHistory } from 'react-router-dom';
import styled from 'styled-components';
import { Row, Box, Col, Text, Anchor, Icon, Action } from '@tlon/indigo-react';
import {
Post,
GraphNotifIndex,
GraphNotificationContents,
Associations,
Content,
Rolodex,
Groups,
} from "~/types";
import { Header } from "./header";
import { cite, deSig, pluralize } from "~/logic/lib/util";
import { Sigil } from "~/logic/lib/sigil";
import RichText from "~/views/components/RichText";
import GlobalApi from "~/logic/api/global";
import ReactMarkdown from "react-markdown";
import { getSnippet } from "~/logic/lib/publish";
import styled from "styled-components";
import {MentionText} from "~/views/components/MentionText";
import ChatMessage, {MessageWithoutSigil} from "../chat/components/ChatMessage";
Groups
} from '@urbit/api';
function getGraphModuleIcon(module: string) {
if (module === "link") {
return "Collection";
import { Header } from './header';
import { cite, deSig, pluralize } from '~/logic/lib/util';
import { Sigil } from '~/logic/lib/sigil';
import GlobalApi from '~/logic/api/global';
import { getSnippet } from '~/logic/lib/publish';
import { MentionText } from '~/views/components/MentionText';
import { MessageWithoutSigil } from '../chat/components/ChatMessage';
function getGraphModuleIcon(module: string): string {
if (module === 'link') {
return 'Collection';
}
return _.capitalize(module);
}
@ -34,32 +32,32 @@ const FilterBox = styled(Box)`
background: linear-gradient(
to bottom,
transparent,
${(p) => p.theme.colors.white}
${p => p.theme.colors.white}
);
`;
function describeNotification(description: string, plural: boolean) {
function describeNotification(description: string, plural: boolean): string {
switch (description) {
case "link":
return `added ${pluralize("new link", plural)} to`;
case "comment":
return `left ${pluralize("comment", plural)} on`;
case "edit-comment":
return `updated ${pluralize("comment", plural)} on`;
case "note":
return `posted ${pluralize("note", plural)} to`;
case "edit-note":
return `updated ${pluralize("note", plural)} in`;
case "mention":
return "mentioned you on";
case "message":
return `sent ${pluralize("message", plural)} to`;
case 'link':
return `added ${pluralize('new link', plural)} to`;
case 'comment':
return `left ${pluralize('comment', plural)} on`;
case 'edit-comment':
return `updated ${pluralize('comment', plural)} on`;
case 'note':
return `posted ${pluralize('note', plural)} to`;
case 'edit-note':
return `updated ${pluralize('note', plural)} in`;
case 'mention':
return 'mentioned you on';
case 'message':
return `sent ${pluralize('message', plural)} to`;
default:
return description;
}
}
const GraphUrl = ({ url, title }) => (
const GraphUrl = ({ url, title }): ReactElement => (
<Box borderRadius="2" p="2" bg="scales.black05">
<Anchor underline={false} target="_blank" color="black" href={url}>
<Icon verticalAlign="bottom" mr="2" icon="ArrowExternal" />
@ -68,10 +66,10 @@ const GraphUrl = ({ url, title }) => (
</Box>
);
const GraphNodeContent = ({ group, post, contacts, mod, description, index, remoteContentPolicy }) => {
const GraphNodeContent = ({ group, post, contacts, mod, index }): ReactElement => {
const { contents } = post;
const idx = index.slice(1).split("/");
if (mod === "link") {
const idx = index.slice(1).split('/');
if (mod === 'link') {
if (idx.length === 1) {
const [{ text }, { url }] = contents;
return <GraphUrl title={text} url={url} />;
@ -80,20 +78,20 @@ const GraphNodeContent = ({ group, post, contacts, mod, description, index, remo
content={contents}
contacts={contacts}
group={group}
/>
/>;
}
return null;
}
if (mod === "publish") {
if (idx[1] === "2") {
if (mod === 'publish') {
if (idx[1] === '2') {
return <MentionText
content={contents}
group={group}
contacts={contacts}
fontSize='14px'
lineHeight="tall"
/>
} else if (idx[1] === "1") {
/>;
} else if (idx[1] === '1') {
const [{ text: header }, { text: body }] = contents;
const snippet = getSnippet(body);
return (
@ -123,42 +121,41 @@ const GraphNodeContent = ({ group, post, contacts, mod, description, index, remo
flexShrink={0}
flexGrow={1}
flexWrap="wrap"
>
>
<MessageWithoutSigil
containerClass="items-top cf hide-child"
measure={() => {}}
group={group}
contacts={contacts}
groups={{}}
associations={{ graph: {}, groups: {}}}
associations={{ graph: {}, groups: {} }}
msg={post}
fontSize='0'
pt='2'
/>
</Row>);
}
return null;
};
function getNodeUrl(mod: string, hidden: boolean, groupPath: string, graph: string, index: string) {
function getNodeUrl(mod: string, hidden: boolean, groupPath: string, graph: string, index: string): string {
if (hidden && mod === 'chat') {
groupPath = '/messages';
} else if (hidden) {
groupPath = '/home';
}
const graphUrl = `/~landscape${groupPath}/resource/${mod}${graph}`;
const idx = index.slice(1).split("/");
if (mod === "publish") {
const idx = index.slice(1).split('/');
if (mod === 'publish') {
const [noteId] = idx;
return `${graphUrl}/note/${noteId}`;
} else if (mod === "link") {
} else if (mod === 'link') {
const [linkId] = idx;
return `${graphUrl}/${linkId}`;
} else if (mod === 'chat') {
return graphUrl;
}
return "";
return '';
}
const GraphNode = ({
post,
@ -174,9 +171,7 @@ const GraphNode = ({
read,
onRead,
showContact = false,
remoteContentPolicy
}) => {
const { contents } = post;
}): ReactElement => {
author = deSig(author);
const history = useHistory();
@ -185,7 +180,7 @@ const GraphNode = ({
ship={`~${author}`}
size={16}
icon
color={`#000000`}
color={'#000000'}
classes="mix-blend-diff"
padding={2}
/>
@ -212,12 +207,12 @@ const GraphNode = ({
alignItems="center"
p="1"
backgroundColor="white"
>
>
<Text mono title={author}>
{cite(author)}
</Text>
<Text ml="2" gray>
{moment(time).format("HH:mm")}
{moment(time).format('HH:mm')}
</Text>
</Row>}
<Row width="100%" p="1" flexDirection="column">
@ -249,7 +244,7 @@ export function GraphNotification(props: {
}) {
const { contents, index, read, time, api, timebox, groups } = props;
const authors = _.map(contents, "author");
const authors = _.map(contents, 'author');
const { graph, group } = index;
const icon = getGraphModuleIcon(index.module);
const desc = describeNotification(index.description, contents.length !== 1);
@ -259,7 +254,7 @@ export function GraphNotification(props: {
return;
}
return api.hark["read"](timebox, { graph: index });
return api.hark['read'](timebox, { graph: index });
}, [api, timebox, index, read]);
return (
@ -284,7 +279,7 @@ return (
author={content.author}
contacts={props.contacts}
mod={index.module}
time={content?.["time-sent"]}
time={content?.['time-sent']}
description={index.description}
index={content.index}
graph={graph}

View File

@ -1,44 +1,34 @@
import React, { ReactNode, useCallback } from "react";
import moment from "moment";
import { Row, Box, Col, Text, Anchor, Icon, Action } from "@tlon/indigo-react";
import _ from "lodash";
import { NotificationProps } from "./types";
import React, { ReactElement, useCallback } from 'react';
import _ from 'lodash';
import { Col } from '@tlon/indigo-react';
import {
Post,
GraphNotifIndex,
GraphNotificationContents,
Associations,
Content,
IndexedNotification,
GroupNotificationContents,
GroupNotifIndex,
GroupUpdate,
Rolodex,
} from "~/types";
import { Header } from "./header";
import { cite, deSig } from "~/logic/lib/util";
import { Sigil } from "~/logic/lib/sigil";
import RichText from "~/views/components/RichText";
import GlobalApi from "~/logic/api/global";
import { StatelessAsyncAction } from "~/views/components/StatelessAsyncAction";
Rolodex
} from '@urbit/api';
import { Header } from './header';
import GlobalApi from '~/logic/api/global';
function describeNotification(description: string, plural: boolean) {
switch (description) {
case "add-members":
return "joined";
case "remove-members":
return "left";
case 'add-members':
return 'joined';
case 'remove-members':
return 'left';
default:
return description;
}
}
function getGroupUpdateParticipants(update: GroupUpdate) {
if ("addMembers" in update) {
function getGroupUpdateParticipants(update: GroupUpdate): string[] {
if ('addMembers' in update) {
return update.addMembers.ships;
}
if ("removeMembers" in update) {
if ('removeMembers' in update) {
return update.removeMembers.ships;
}
return [];
@ -56,7 +46,7 @@ interface GroupNotificationProps {
api: GlobalApi;
}
export function GroupNotification(props: GroupNotificationProps) {
export function GroupNotification(props: GroupNotificationProps): ReactElement {
const { contents, index, read, time, api, timebox, associations } = props;
const authors = _.flatten(_.map(contents, getGroupUpdateParticipants));
@ -68,7 +58,7 @@ export function GroupNotification(props: GroupNotificationProps) {
if (props.archived) {
return;
}
const func = read ? "unread" : "read";
const func = read ? 'unread' : 'read';
return api.hark[func](timebox, { group: index });
}, [api, timebox, index, read]);

View File

@ -1,17 +1,19 @@
import React from "react";
import { Text as NormalText, Row, Icon, Rule, Box } from "@tlon/indigo-react";
import f from "lodash/fp";
import _ from "lodash";
import moment from "moment";
import { PropFunc } from "~/types/util";
import { getContactDetails, useShowNickname } from "~/logic/lib/util";
import { Associations, Contact, Contacts, Rolodex } from "~/types";
import React, { ReactElement } from 'react';
import f from 'lodash/fp';
import _ from 'lodash';
import moment from 'moment';
import { Text as NormalText, Row, Icon, Rule } from '@tlon/indigo-react';
import { Associations, Contact, Contacts, Rolodex } from '@urbit/api';
import { PropFunc } from '~/types/util';
import { useShowNickname } from '~/logic/lib/util';
const Text = (props: PropFunc<typeof Text>) => (
<NormalText fontWeight="500" {...props} />
);
function Author(props: { patp: string; contacts: Contacts; last?: boolean }) {
function Author(props: { patp: string; contacts: Contacts; last?: boolean }): ReactElement {
const contact: Contact | undefined = props.contacts?.[props.patp];
const showNickname = useShowNickname(contact);
@ -20,7 +22,7 @@ function Author(props: { patp: string; contacts: Contacts; last?: boolean }) {
return (
<Text mono={!showNickname}>
{name}
{!props.last && ", "}
{!props.last && ', '}
</Text>
);
}
@ -36,7 +38,7 @@ export function Header(props: {
time: number;
read: boolean;
associations: Associations;
} & PropFunc<typeof Row> ) {
} & PropFunc<typeof Row> ): ReactElement {
const { description, channel, group, moduleIcon, read } = props;
const contacts = props.contacts[group] || {};
@ -50,17 +52,17 @@ export function Header(props: {
const last = lent - 1 === parseInt(idx, 10);
return <Author key={idx} contacts={contacts} patp={p} last={last} />;
}),
(auths) => (
auths => (
<React.Fragment>
{auths}
{authors.length > 3 &&
` and ${authors.length - 3} other${authors.length === 4 ? "" : "s"}`}
` and ${authors.length - 3} other${authors.length === 4 ? '' : 's'}`}
</React.Fragment>
)
)(authors);
const time = moment(props.time).format("HH:mm");
const time = moment(props.time).format('HH:mm');
const groupTitle =
props.associations.groups?.[props.group]?.metadata?.title;
@ -84,8 +86,8 @@ export function Header(props: {
{authorDesc}
</Text>
<Text mr="1">{description}</Text>
{!!moduleIcon && <Icon icon={moduleIcon as any} mr={1} />}
{!!channel && <Text fontWeight="500" mr={1}>{channelTitle}</Text>}
{Boolean(moduleIcon) && <Icon icon={moduleIcon as any} mr={1} />}
{Boolean(channel) && <Text fontWeight="500" mr={1}>{channelTitle}</Text>}
<Rule vertical height="12px" mr={1} />
{groupTitle &&
<>

View File

@ -1,22 +1,28 @@
import React, { useEffect, useCallback, useRef, useState } from "react";
import f from "lodash/fp";
import _ from "lodash";
import { Icon, Col, Center, Row, Box, Text, Anchor, Rule, LoadingSpinner } from "@tlon/indigo-react";
import moment from "moment";
import { Notifications, Rolodex, Timebox, IndexedNotification, Groups, joinProgress, JoinRequests, GroupNotificationsConfig, NotificationGraphConfig } from "~/types";
import { MOMENT_CALENDAR_DATE, daToUnix, resourceAsPath } from "~/logic/lib/util";
import { BigInteger } from "big-integer";
import GlobalApi from "~/logic/api/global";
import { Notification } from "./notification";
import { Associations } from "~/types";
import { InviteItem } from '~/views/components/Invite';
import { useWaitForProps } from "~/logic/lib/useWaitForProps";
import { useHistory } from "react-router-dom";
import {useModal} from "~/logic/lib/useModal";
import {JoinGroup} from "~/views/landscape/components/JoinGroup";
import {JoiningStatus} from "./joining";
import {Invites} from "./invites";
import {useLazyScroll} from "~/logic/lib/useLazyScroll";
import React, { useEffect, useCallback, useRef } from 'react';
import f from 'lodash/fp';
import _ from 'lodash';
import moment from 'moment';
import { BigInteger } from 'big-integer';
import { Col, Center, Box, Text, LoadingSpinner } from '@tlon/indigo-react';
import {
Associations,
Notifications,
Rolodex,
Timebox,
IndexedNotification,
Groups,
JoinRequests,
GroupNotificationsConfig,
NotificationGraphConfig,
Invites as InviteType
} from '@urbit/api';
import { MOMENT_CALENDAR_DATE, daToUnix } from '~/logic/lib/util';
import GlobalApi from '~/logic/api/global';
import { Notification } from './notification';
import { Invites } from './invites';
import { useLazyScroll } from '~/logic/lib/useLazyScroll';
type DatedTimebox = [BigInteger, Timebox];
@ -25,12 +31,12 @@ function filterNotification(associations: Associations, groups: string[]) {
return () => true;
}
return (n: IndexedNotification) => {
if ("graph" in n.index) {
if ('graph' in n.index) {
const { group } = n.index.graph;
return groups.findIndex((g) => group === g) !== -1;
} else if ("group" in n.index) {
return groups.findIndex(g => group === g) !== -1;
} else if ('group' in n.index) {
const { group } = n.index.group;
return groups.findIndex((g) => group === g) !== -1;
return groups.findIndex(g => group === g) !== -1;
}
return true;
};
@ -46,7 +52,7 @@ export default function Inbox(props: {
associations: Associations;
contacts: Rolodex;
filter: string[];
invites: any;
invites: InviteType;
pendingJoin: JoinRequests;
notificationsGroupConfig: GroupNotificationsConfig;
notificationsGraphConfig: NotificationGraphConfig;
@ -70,30 +76,30 @@ export default function Inbox(props: {
const calendar = {
...MOMENT_CALENDAR_DATE, sameDay: function (now) {
if (this.subtract(6, 'hours').isBefore(now)) {
return "[Earlier Today]";
return '[Earlier Today]';
} else {
return MOMENT_CALENDAR_DATE.sameDay;
}
}
};
let notificationsByDay = f.flow(
const notificationsByDay = f.flow(
f.map<DatedTimebox, DatedTimebox>(([date, nots]) => [
date,
nots.filter(filterNotification(associations, props.filter)),
nots.filter(filterNotification(associations, props.filter))
]),
f.groupBy<DatedTimebox>(([d]) => {
const date = moment(daToUnix(d));
if (moment().subtract(6, 'hours').isBefore(date)) {
return 'latest';
} else {
return date.format("YYYYMMDD");
return date.format('YYYYMMDD');
}
}),
})
)(notifications);
const notificationsByDayMap = new Map<string, DatedTimebox[]>(
Object.keys(notificationsByDay).map(timebox => {
Object.keys(notificationsByDay).map((timebox) => {
return [timebox, notificationsByDay[timebox]];
})
);
@ -105,13 +111,12 @@ export default function Inbox(props: {
}, [api]);
const { isDone, isLoading } = useLazyScroll(
scrollRef,
scrollRef,
0.2,
_.flatten(notifications).length,
loadMore
);
return (
<Col ref={scrollRef} position="relative" height="100%" overflowY="auto">
<Invites groups={props.groups} pendingJoin={props.pendingJoin} invites={invites} api={api} associations={associations} />
@ -123,7 +128,7 @@ export default function Inbox(props: {
label={day === 'latest' ? 'Today' : moment(day).calendar(null, calendar)}
timeboxes={timeboxes}
contacts={props.contacts}
archive={!!props.showArchive}
archive={Boolean(props.showArchive)}
associations={props.associations}
api={api}
groups={props.groups}
@ -142,7 +147,7 @@ export default function Inbox(props: {
<LoadingSpinner />
</Center>
)}
</Col>
);
}
@ -167,9 +172,8 @@ function DaySection({
associations,
api,
groupConfig,
graphConfig,
graphConfig
}) {
const lent = timeboxes.map(([,nots]) => nots.length).reduce(f.add, 0);
if (lent === 0 || timeboxes.length === 0) {
return null;

View File

@ -1,15 +1,12 @@
import React, { useCallback, useState } from "react";
import React, { ReactElement } from 'react';
import _ from 'lodash';
import { Box, Row, Col } from "@tlon/indigo-react";
import GlobalApi from "~/logic/api/global";
import { Invites as IInvites, Associations, Invite, JoinRequests, Groups, Contacts, AppInvites, JoinProgress } from "~/types";
import { resourceAsPath, alphabeticalOrder } from "~/logic/lib/util";
import { useHistory } from "react-router-dom";
import { useWaitForProps } from "~/logic/lib/useWaitForProps";
import InviteItem from "~/views/components/Invite";
import {JoiningStatus} from "./joining";
import {useModal} from "~/logic/lib/useModal";
import {JoinGroup} from "~/views/landscape/components/JoinGroup";
import { Col } from '@tlon/indigo-react';
import { Invites as IInvites, Associations, Invite, JoinRequests, Groups, Contacts, AppInvites, JoinProgress } from '@urbit/api';
import GlobalApi from '~/logic/api/global';
import { resourceAsPath, alphabeticalOrder } from '~/logic/lib/util';
import InviteItem from '~/views/components/Invite';
interface InvitesProps {
api: GlobalApi;
@ -26,50 +23,18 @@ interface InviteRef {
invite: Invite;
}
export function Invites(props: InvitesProps) {
export function Invites(props: InvitesProps): ReactElement {
const { api, invites, pendingJoin } = props;
const [selected, setSelected] = useState<[string, string, Invite] | undefined>()
const acceptInvite = (
app: string,
uid: string,
invite: Invite
) => async () => {
setSelected([app, uid, invite]);
showModal();
};
const declineInvite = useCallback(
(app: string, uid: string) => () => api.invite.decline(app, uid),
[api]
);
const { modal, showModal } = useModal({ modal: () => {
const [app, uid, invite] = selected!;
const autojoin = `~${invite.resource.ship}/${invite.resource.name}`;
return (
<JoinGroup
groups={props.groups}
associations={props.associations}
api={api}
autojoin={autojoin}
inviteUid={uid}
inviteApp={app}
/>
)}});
const inviteArr: InviteRef[] = _.reduce(invites, (acc: InviteRef[], val: AppInvites, app: string) => {
const appInvites = _.reduce(val, (invs: InviteRef[], invite: Invite, uid: string) => {
return [...invs, { invite, uid, app }];
}, []);
return [...acc, ...appInvites];
}, []);
const invitesAndStatus: { [rid: string]: JoinProgress | InviteRef } =
{..._.keyBy(inviteArr, ({ invite }) => resourceAsPath(invite.resource)), ...props.pendingJoin };
const invitesAndStatus: { [rid: string]: JoinProgress | InviteRef } =
{ ..._.keyBy(inviteArr, ({ invite }) => resourceAsPath(invite.resource)), ...props.pendingJoin };
return (
<Col
@ -83,7 +48,7 @@ export function Invites(props: InvitesProps) {
{ Object
.keys(invitesAndStatus)
.sort(alphabeticalOrder)
.map(resource => {
.map((resource) => {
const inviteOrStatus = invitesAndStatus[resource];
if(typeof inviteOrStatus === 'string') {
return (
@ -93,10 +58,10 @@ export function Invites(props: InvitesProps) {
groups={props.groups}
associations={props.associations}
resource={resource}
pendingJoin={pendingJoin}
api={api} />
)
pendingJoin={pendingJoin}
api={api}
/>
);
} else {
const { app, uid, invite } = inviteOrStatus;
console.log(inviteOrStatus);
@ -107,13 +72,13 @@ export function Invites(props: InvitesProps) {
invite={invite}
app={app}
uid={uid}
pendingJoin={pendingJoin}
pendingJoin={pendingJoin}
resource={resource}
contacts={props.contacts}
groups={props.groups}
associations={props.associations}
/>
)
);
}
})}
</Col>

View File

@ -1,35 +1,32 @@
import React, { useState, useEffect } from "react";
import { Col, Row, Text, SegmentedProgressBar, Box } from "@tlon/indigo-react";
import GlobalApi from "~/logic/api/global";
import React from 'react';
import { Row, Text, SegmentedProgressBar, Box } from '@tlon/indigo-react';
import {
JoinProgress,
joinProgress,
MetadataUpdatePreview,
joinError,
} from "~/types";
import { clamp } from "~/logic/lib/util";
joinError
} from '@urbit/api';
interface JoiningStatusProps {
status: JoinProgress;
}
const description: string[] = [
"Attempting to contact host",
"Retrieving data",
"Finished join",
"Unable to join, you do not have the correct permissions",
"Internal error, please file an issue",
'Attempting to contact host',
'Retrieving data',
'Finished join',
'Unable to join, you do not have the correct permissions',
'Internal error, please file an issue'
];
export function JoiningStatus(props: JoiningStatusProps) {
const { status } = props;
const current = joinProgress.indexOf(status);
const desc = description?.[current] || "";
const desc = description?.[current] || '';
const isError = joinError.indexOf(status as any) !== -1;
return (
<Row
display={["flex-column", "flex"]}
display={['flex-column', 'flex']}
alignItems="center"
px="4"
gapX="4"
@ -37,7 +34,7 @@ export function JoiningStatus(props: JoiningStatusProps) {
<Box flexGrow={1} maxWidth="400px">
<SegmentedProgressBar current={current + 1} segments={3} />
</Box>
<Text display="block" flexShrink={0} color={isError ? "red" : "gray"}>
<Text display="block" flexShrink={0} color={isError ? 'red' : 'gray'}>
{desc}
</Text>
</Row>

View File

@ -1,8 +1,8 @@
import React from "react";
import { Box } from "@tlon/indigo-react";
import React from 'react';
import { Box } from '@tlon/indigo-react';
import { MetadataBody, NotificationProps } from "./types";
import { Header } from "./header";
import { MetadataBody, NotificationProps } from './types';
import { Header } from './header';
function getInvolvedUsers(body: MetadataBody) {
return [];
@ -10,22 +10,22 @@ function getInvolvedUsers(body: MetadataBody) {
function getDescription(body: MetadataBody) {
const b = body.metadata;
if ("new" in b) {
return "created";
} else if ("changedTitle" in b) {
return "changed the title to";
} else if ("changedDescription" in b) {
return "changed the description to";
} else if ("changedColor" in b) {
return "changed the color to";
} else if ("deleted" in b) {
return "deleted";
if ('new' in b) {
return 'created';
} else if ('changedTitle' in b) {
return 'changed the title to';
} else if ('changedDescription' in b) {
return 'changed the description to';
} else if ('changedColor' in b) {
return 'changed the color to';
} else if ('deleted' in b) {
return 'deleted';
} else {
throw new Error("bad metadata frond");
throw new Error('bad metadata frond');
}
}
export function MetadataNotification(props: NotificationProps<"metadata">) {
export function MetadataNotification(props: NotificationProps<'metadata'>) {
const { unread } = props;
const description = getDescription(unread.unreads[0].body);

View File

@ -1,6 +1,6 @@
import React, { ReactNode, useCallback, useMemo, useState } from "react";
import { Row, Box } from "@tlon/indigo-react";
import _ from "lodash";
import React, { ReactNode, useCallback, useMemo, useState } from 'react';
import { Row, Box } from '@tlon/indigo-react';
import _ from 'lodash';
import {
GraphNotificationContents,
IndexedNotification,
@ -9,15 +9,15 @@ import {
GroupNotificationsConfig,
Groups,
Associations,
Contacts,
} from "~/types";
import GlobalApi from "~/logic/api/global";
import { getParentIndex } from "~/logic/lib/notification";
import { StatelessAsyncAction } from "~/views/components/StatelessAsyncAction";
import { GroupNotification } from "./group";
import { GraphNotification } from "./graph";
import { BigInteger } from "big-integer";
import { useHovering } from "~/logic/lib/util";
Contacts
} from '@urbit/api';
import GlobalApi from '~/logic/api/global';
import { getParentIndex } from '~/logic/lib/notification';
import { StatelessAsyncAction } from '~/views/components/StatelessAsyncAction';
import { GroupNotification } from './group';
import { GraphNotification } from './graph';
import { BigInteger } from 'big-integer';
import { useHovering } from '~/logic/lib/util';
interface NotificationProps {
notification: IndexedNotification;
@ -37,7 +37,7 @@ function getMuted(
graphs: NotificationGraphConfig
) {
const { index, notification } = idxNotif;
if ("graph" in idxNotif.index) {
if ('graph' in idxNotif.index) {
const { graph } = idxNotif.index.graph;
if(!('graph' in notification.contents)) {
throw new Error();
@ -46,11 +46,11 @@ function getMuted(
return _.findIndex(
graphs?.watching || [],
(g) => g.graph === graph && g.index === parent
g => g.graph === graph && g.index === parent
) === -1;
}
if ("group" in index) {
return _.findIndex(groups || [], (g) => g === index.group.group) === -1;
if ('group' in index) {
return _.findIndex(groups || [], g => g === index.group.group) === -1;
}
return false;
}
@ -77,13 +77,13 @@ function NotificationWrapper(props: {
);
const onChangeMute = useCallback(async () => {
const func = isMuted ? "unmute" : "mute";
const func = isMuted ? 'unmute' : 'mute';
return api.hark[func](notif);
}, [notif, api, isMuted]);
const { hovering, bind } = useHovering();
const changeMuteDesc = isMuted ? "Unmute" : "Mute";
const changeMuteDesc = isMuted ? 'Unmute' : 'Mute';
return (
<Box
width="100%"
@ -126,7 +126,7 @@ export function Notification(props: NotificationProps) {
</NotificationWrapper>
);
if ("graph" in notification.index) {
if ('graph' in notification.index) {
const index = notification.index.graph;
const c: GraphNotificationContents = (contents as any).graph;
@ -147,7 +147,7 @@ export function Notification(props: NotificationProps) {
</Wrapper>
);
}
if ("group" in notification.index) {
if ('group' in notification.index) {
const index = notification.index.group;
const c: GroupNotificationContents = (contents as any).group;
return (

View File

@ -1,25 +1,25 @@
import React, { useCallback, useState, useRef } from "react";
import React, { useCallback, useState, useRef, ReactElement } 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 { Link, Switch, Route } from 'react-router-dom';
import Helmet from 'react-helmet';
import { Body } from "~/views/components/Body";
import { PropFunc } from "~/types/util";
import Inbox from "./inbox";
import NotificationPreferences from "./preferences";
import { Dropdown } from "~/views/components/Dropdown";
import { Formik } from "formik";
import { FormikOnBlur } from "~/views/components/FormikOnBlur";
import GroupSearch from "~/views/components/GroupSearch";
import {useTutorialModal} from "~/views/components/useTutorialModal";
import { Box, Col, Text, Row } from '@tlon/indigo-react';
const baseUrl = "/~notifications";
import { Body } from '~/views/components/Body';
import { PropFunc } from '~/types/util';
import Inbox from './inbox';
import NotificationPreferences from './preferences';
import { Dropdown } from '~/views/components/Dropdown';
import { FormikOnBlur } from '~/views/components/FormikOnBlur';
import GroupSearch from '~/views/components/GroupSearch';
import { useTutorialModal } from '~/views/components/useTutorialModal';
const baseUrl = '/~notifications';
const HeaderLink = React.forwardRef((
props: PropFunc<typeof Text> & { view?: string; current: string },
ref
) => {
): ReactElement => {
const { current, view, ...textProps } = props;
const to = view ? `${baseUrl}/${view}` : baseUrl;
const active = view ? current === view : !current;
@ -35,7 +35,7 @@ interface NotificationFilter {
groups: string[];
}
export default function NotificationsScreen(props: any) {
export default function NotificationsScreen(props: any): ReactElement {
const relativePath = (p: string) => baseUrl + p;
const [filter, setFilter] = useState<NotificationFilter>({ groups: [] });
@ -43,20 +43,20 @@ export default function NotificationsScreen(props: any) {
setFilter({ groups });
};
const onReadAll = useCallback(() => {
props.api.hark.readAll()
props.api.hark.readAll();
}, []);
const groupFilterDesc =
filter.groups.length === 0
? "All"
? 'All'
: filter.groups
.map((g) => props.associations?.groups?.[g]?.metadata?.title)
.join(", ");
.map(g => props.associations?.groups?.[g]?.metadata?.title)
.join(', ');
const anchorRef = useRef<HTMLElement | null>(null);
useTutorialModal('notifications', true, anchorRef.current);
return (
<Switch>
<Route
path={[relativePath("/:view"), relativePath("")]}
path={[relativePath('/:view'), relativePath('')]}
render={(routeProps) => {
const { view } = routeProps.match.params;
return (
@ -89,7 +89,8 @@ export default function NotificationsScreen(props: any) {
</Box>
</Row>
<Row
justifyContent="space-between">
justifyContent="space-between"
>
<Box
mr="1"
overflow="hidden"
@ -136,7 +137,7 @@ export default function NotificationsScreen(props: any) {
</Dropdown>
</Row>
</Row>
{view === "preferences" && (
{view === 'preferences' && (
<NotificationPreferences
graphConfig={props.notificationsGraphConfig}
api={props.api}

View File

@ -1,13 +1,12 @@
import React, { useCallback } from "react";
import React, { ReactElement, useCallback } from 'react';
import { Form, FormikHelpers } from 'formik';
import _ from 'lodash';
import { Box, Col, ManagedCheckboxField as Checkbox } from "@tlon/indigo-react";
import { Formik, Form, FormikHelpers } from "formik";
import * as Yup from "yup";
import _ from "lodash";
import { AsyncButton } from "~/views/components/AsyncButton";
import { FormikOnBlur } from "~/views/components/FormikOnBlur";
import { NotificationGraphConfig } from "~/types";
import GlobalApi from "~/logic/api/global";
import { Col, ManagedCheckboxField as Checkbox } from '@tlon/indigo-react';
import { NotificationGraphConfig } from '@urbit/api';
import { FormikOnBlur } from '~/views/components/FormikOnBlur';
import GlobalApi from '~/logic/api/global';
interface FormSchema {
mentions: boolean;
@ -24,21 +23,21 @@ interface NotificationPreferencesProps {
export default function NotificationPreferences(
props: NotificationPreferencesProps
) {
): ReactElement {
const { graphConfig, api, dnd } = props;
const initialValues: FormSchema = {
mentions: graphConfig.mentions,
watchOnSelf: graphConfig.watchOnSelf,
dnd,
watching: graphConfig.watching,
watching: graphConfig.watching
};
const onSubmit = useCallback(
async (values: FormSchema, actions: FormikHelpers<FormSchema>) => {
console.log(values);
try {
let promises: Promise<any>[] = [];
const promises: Promise<any>[] = [];
if (values.mentions !== graphConfig.mentions) {
promises.push(api.hark.setMentions(values.mentions));
}
@ -46,7 +45,7 @@ export default function NotificationPreferences(
promises.push(api.hark.setWatchOnSelf(values.watchOnSelf));
}
if (values.dnd !== dnd && !_.isUndefined(values.dnd)) {
promises.push(api.hark.setDoNotDisturb(values.dnd))
promises.push(api.hark.setDoNotDisturb(values.dnd));
}
await Promise.all(promises);

View File

@ -1,30 +1,25 @@
import React from "react";
import * as Yup from "yup";
import React, { ReactElement } from 'react';
import * as Yup from 'yup';
import _ from 'lodash';
import { Formik } from 'formik';
import { useHistory } from 'react-router-dom';
import {
ManagedForm as Form,
ManagedTextInputField as Input,
ManagedCheckboxField as Checkbox,
Center,
Col,
Box,
Text,
Row,
Button,
} from "@tlon/indigo-react";
import { Formik, FormikHelpers } from "formik";
import { useHistory } from "react-router-dom";
import { uxToHex } from "~/logic/lib/util";
import { Sigil } from "~/logic/lib/sigil";
import { AsyncButton } from "~/views/components/AsyncButton";
import { ColorInput } from "~/views/components/ColorInput";
import { ImageInput } from "~/views/components/ImageInput";
import { MarkdownField } from "~/views/apps/publish/components/MarkdownField";
import { resourceFromPath } from "~/logic/lib/group";
import GroupSearch from "~/views/components/GroupSearch";
} from '@tlon/indigo-react';
import { uxToHex } from '~/logic/lib/util';
import { AsyncButton } from '~/views/components/AsyncButton';
import { ColorInput } from '~/views/components/ColorInput';
import { ImageInput } from '~/views/components/ImageInput';
import { MarkdownField } from '~/views/apps/publish/components/MarkdownField';
import { resourceFromPath } from '~/logic/lib/group';
import GroupSearch from '~/views/components/GroupSearch';
const formSchema = Yup.object({
nickname: Yup.string(),
@ -45,8 +40,7 @@ const emptyContact = {
isPublic: false
};
export function EditProfile(props: any) {
export function EditProfile(props: any): ReactElement {
const { contact, ship, api, isPublic } = props;
const history = useHistory();
if (contact) {
@ -58,10 +52,10 @@ export function EditProfile(props: any) {
try {
await Object.keys(values).reduce((acc, key) => {
console.log(key);
const newValue = key !== "color" ? values[key] : uxToHex(values[key]);
const newValue = key !== 'color' ? values[key] : uxToHex(values[key]);
if (newValue !== contact[key]) {
if (key === "isPublic") {
if (key === 'isPublic') {
return acc.then(() =>
api.contacts.setPublic(newValue)
);
@ -70,23 +64,22 @@ export function EditProfile(props: any) {
console.log(toRemove);
const toAdd: string[] = _.difference(newValue, contact?.groups || []);
console.log(toAdd);
let promises: Promise<any>[] = [];
const promises: Promise<any>[] = [];
promises.concat(
toRemove.map(e =>
api.contacts.edit(ship, {'remove-group': resourceFromPath(e) })
api.contacts.edit(ship, { 'remove-group': resourceFromPath(e) })
)
);
promises.concat(
toAdd.map(e =>
api.contacts.edit(ship, {'add-group': resourceFromPath(e) })
api.contacts.edit(ship, { 'add-group': resourceFromPath(e) })
)
);
return acc.then(() => Promise.all(promises));
} else if (
key !== "last-updated" &&
key !== "isPublic"
key !== 'last-updated' &&
key !== 'isPublic'
) {
return acc.then(() =>
api.contacts.edit(ship, { [key]: newValue })
@ -95,7 +88,7 @@ export function EditProfile(props: any) {
}
return acc;
}, Promise.resolve());
//actions.setStatus({ success: null });
// actions.setStatus({ success: null });
history.push(`/~profile/${ship}`);
} catch (e) {
console.error(e);

View File

@ -1,25 +1,23 @@
import React, { useEffect, useRef, useState } from "react";
import { Sigil } from "~/logic/lib/sigil";
import { ViewProfile } from './ViewProfile';
import { EditProfile } from './EditProfile';
import { SetStatusBarModal } from '~/views/components/SetStatusBarModal';
import React, { ReactElement, useEffect, useRef, useState } from 'react';
import { useHistory } from 'react-router-dom';
import { uxToHex } from "~/logic/lib/util";
import {
Center,
Box,
Row,
BaseImage,
StatelessTextInput as Input,
Button,
Text
} from "@tlon/indigo-react";
import useLocalState from "~/logic/state/local";
import { useHistory } from "react-router-dom";
import {useTutorialModal} from "~/views/components/useTutorialModal";
} from '@tlon/indigo-react';
import { Sigil } from '~/logic/lib/sigil';
import { ViewProfile } from './ViewProfile';
import { EditProfile } from './EditProfile';
import { SetStatusBarModal } from '~/views/components/SetStatusBarModal';
import { uxToHex } from '~/logic/lib/util';
import useLocalState from '~/logic/state/local';
import { useTutorialModal } from '~/views/components/useTutorialModal';
export function Profile(props: any) {
export function Profile(props: any): ReactElement {
const { hideAvatars } = useLocalState(({ hideAvatars }) => ({
hideAvatars
}));
@ -31,17 +29,13 @@ export function Profile(props: any) {
const { contact, nackedContacts, hasLoaded, isPublic, isEdit, ship } = props;
const nacked = nackedContacts.has(ship);
const [statusModal, showStatusModal] = useState(false);
useEffect(() => {
if(hasLoaded && !contact && !nacked) {
props.api.contacts.retrieve(ship);
}
}, [hasLoaded, contact])
}, [hasLoaded, contact]);
const hexColor = contact?.color ? `#${uxToHex(contact.color)}` : "#000000";
const hexColor = contact?.color ? `#${uxToHex(contact.color)}` : '#000000';
const cover = (contact?.cover)
? <BaseImage src={contact.cover} width='100%' height='100%' style={{ objectFit: 'cover' }} />
: <Box display="block" width='100%' height='100%' backgroundColor='washedGray' />;
@ -58,19 +52,24 @@ export function Profile(props: any) {
<Center
p={[0,4]}
height="100%"
width="100%">
width="100%"
>
<Box
ref={anchorRef}
maxWidth="600px"
width="100%">
width="100%"
>
<Row alignItems="center" justifyContent="space-between">
{ship === `~${window.ship}` ? (
<Row>
<Text
py='2'
cursor='pointer'
onClick={() => { history.push(`/~profile/${ship}/edit`) }}>
onClick={() => {
history.push(`/~profile/${ship}/edit`);
}}
>
Edit Profile
</Text>
<SetStatusBarModal
@ -82,9 +81,12 @@ export function Profile(props: any) {
/>
</Row>
) : null}
<Text maxWidth='18rem' overflowX='hidden' textOverflow="ellipsis"
<Text maxWidth='18rem' overflowX='hidden'
textOverflow="ellipsis"
whiteSpace="nowrap"
overflow="hidden" display="inline-block" verticalAlign="middle">{contact?.status ?? ""}</Text>
overflow="hidden" display="inline-block"
verticalAlign="middle"
>{contact?.status ?? ''}</Text>
</Row>
<Row width="100%" height="300px">
{cover}
@ -108,7 +110,8 @@ export function Profile(props: any) {
api={props.api}
groups={props.groups}
associations={props.associations}
isPublic={isPublic}/>
isPublic={isPublic}
/>
) : (
<ViewProfile
api={props.api}

View File

@ -3,14 +3,13 @@ import React, {
useCallback,
useEffect,
ChangeEvent
} from "react";
} from 'react';
import {
Row,
Button,
StatelessTextInput as Input,
} from "@tlon/indigo-react";
StatelessTextInput as Input
} from '@tlon/indigo-react';
export function SetStatus(props: any) {
const { contact, ship, api, callback } = props;
@ -23,11 +22,11 @@ export function SetStatus(props: any) {
);
useEffect(() => {
setStatus(!!contact ? contact.status : '');
setStatus(contact ? contact.status : '');
}, [contact]);
const editStatus = () => {
api.contacts.edit(ship, {status: _status});
api.contacts.edit(ship, { status: _status });
if (callback) {
callback();
@ -53,7 +52,8 @@ export function SetStatus(props: any) {
color="white"
ml={2}
width="25%"
onClick={editStatus}>
onClick={editStatus}
>
Set Status
</Button>
</Row>

View File

@ -1,28 +1,21 @@
import React, {useEffect, useState} from "react";
import React from 'react';
import _ from 'lodash';
import { Sigil } from "~/logic/lib/sigil";
import { useHistory } from 'react-router-dom';
import {
Center,
Box,
Text,
Row,
Button,
Col,
LoadingSpinner
} from "@tlon/indigo-react";
import { AsyncButton } from "~/views/components/AsyncButton";
import RichText from "~/views/components/RichText";
import { useHistory } from "react-router-dom";
import {GroupSummary} from "~/views/landscape/components/GroupSummary";
import {MetadataUpdatePreview} from "~/types";
import {GroupLink} from "~/views/components/GroupLink";
import {lengthOrder} from "~/logic/lib/util";
import useLocalState from "~/logic/state/local";
} from '@tlon/indigo-react';
import RichText from '~/views/components/RichText';
import { GroupLink } from '~/views/components/GroupLink';
import { lengthOrder } from '~/logic/lib/util';
import useLocalState from '~/logic/state/local';
export function ViewProfile(props: any) {
const history = useHistory();
export function ViewProfile(props: any): ReactElement {
const { hideNicknames } = useLocalState(({ hideNicknames }) => ({
hideNicknames
}));
@ -33,17 +26,19 @@ export function ViewProfile(props: any) {
<Row
pb={2}
alignItems="center"
width="100%">
width="100%"
>
<Center width="100%">
<Text>
{((!hideNicknames && contact?.nickname) ? contact.nickname : "")}
{((!hideNicknames && contact?.nickname) ? contact.nickname : '')}
</Text>
</Center>
</Row>
<Row
pb={2}
alignItems="center"
width="100%">
width="100%"
>
<Center width="100%">
<Text mono color="darkGray">{ship}</Text>
</Center>
@ -52,10 +47,11 @@ export function ViewProfile(props: any) {
pb={2}
alignItems="center"
justifyContent="center"
width="100%">
width="100%"
>
<Center flexDirection="column" maxWidth='32rem'>
<RichText width='100%' disableRemoteContent>
{(contact?.bio ? contact.bio : "")}
{(contact?.bio ? contact.bio : '')}
</RichText>
</Center>
</Col>
@ -82,7 +78,8 @@ export function ViewProfile(props: any) {
borderRadius={1}
bg="white"
border={1}
borderColor="washedGray">
borderColor="washedGray"
>
<Center height="100%">
<Text mono pr={1} color="gray">{ship}</Text>
<Text color="gray">remains private</Text>

View File

@ -1,34 +1,24 @@
import React from "react";
import { Route, Link } from "react-router-dom";
import React from 'react';
import { Route, Link } from 'react-router-dom';
import Helmet from 'react-helmet';
import { Box, Text, Row, Col, Icon, BaseImage } from "@tlon/indigo-react";
import { Box } from '@tlon/indigo-react';
import { uxToHex } from "~/logic/lib/util";
import { Profile } from "./components/Profile";
import useLocalState from "~/logic/state/local";
import { Profile } from './components/Profile';
export default function ProfileScreen(props: any) {
const { dark } = props;
const hideAvatars = useLocalState(state => state.hideAvatars);
return (
<>
<Helmet defer={false}>
<title>{ props.notificationsCount ? `(${String(props.notificationsCount) }) `: '' }Landscape - Profile</title>
</Helmet>
<Route
path={"/~profile/:ship/:edit?"}
render={({ match, history }) => {
path={'/~profile/:ship/:edit?'}
render={({ match }) => {
const ship = match.params.ship;
const isEdit = match.url.includes('edit');
const isPublic = props.isContactPublic;
const contact = props.contacts?.[ship];
const sigilColor = contact?.color
? `#${uxToHex(contact.color)}`
: dark
? "#FFFFFF"
: "#000000";
return (
<Box height="100%" px={[0, 3]} pb={[0, 3]} borderRadius={1}>

View File

@ -1,11 +1,11 @@
import React from "react";
import { Box } from "@tlon/indigo-react";
import React from 'react';
import { Box } from '@tlon/indigo-react';
import GlobalApi from "~/logic/api/global";
import { StoreState } from "~/logic/store/type";
import { Association } from "~/types";
import { RouteComponentProps } from "react-router-dom";
import { NotebookRoutes } from "./components/NotebookRoutes";
import GlobalApi from '~/logic/api/global';
import { StoreState } from '~/logic/store/type';
import { Association } from '@urbit/api';
import { RouteComponentProps } from 'react-router-dom';
import { NotebookRoutes } from './components/NotebookRoutes';
type PublishResourceProps = StoreState & {
association: Association;
@ -16,7 +16,7 @@ type PublishResourceProps = StoreState & {
export function PublishResource(props: PublishResourceProps) {
const { association, api, baseUrl, notebooks } = props;
const rid = association.resource;
const [, , ship, book] = rid.split("/");
const [, , ship, book] = rid.split('/');
const notebookContacts = props.contacts[association.group];
return (

View File

@ -1,12 +1,16 @@
import React from "react";
import React, { ReactElement } from 'react';
import _ from 'lodash';
import { PostFormSchema, PostForm } from "./NoteForm";
import { FormikHelpers } from "formik";
import GlobalApi from "~/logic/api/global";
import { RouteComponentProps, useLocation } from "react-router-dom";
import { GraphNode, TextContent, Association, S3State } from "~/types";
import { getLatestRevision, editPost } from "~/logic/lib/publish";
import {useWaitForProps} from "~/logic/lib/useWaitForProps";
import { FormikHelpers } from 'formik';
import { RouteComponentProps, useLocation } from 'react-router-dom';
import { GraphNode } from '@urbit/api';
import { PostFormSchema, PostForm } from './NoteForm';
import GlobalApi from '~/logic/api/global';
import { getLatestRevision, editPost } from '~/logic/lib/publish';
import { useWaitForProps } from '~/logic/lib/useWaitForProps';
import { S3State } from '~/types';
interface EditPostProps {
ship: string;
noteId: number;
@ -16,7 +20,7 @@ interface EditPostProps {
s3: S3State;
}
export function EditPost(props: EditPostProps & RouteComponentProps) {
export function EditPost(props: EditPostProps & RouteComponentProps): ReactElement {
const { note, book, noteId, api, ship, history, s3 } = props;
const [revNum, title, body] = getLatestRevision(note);
const location = useLocation();
@ -24,19 +28,19 @@ export function EditPost(props: EditPostProps & RouteComponentProps) {
const waiter = useWaitForProps(props);
const initial: PostFormSchema = {
title,
body,
body
};
const onSubmit = async (
values: PostFormSchema,
actions: FormikHelpers<PostFormSchema>
) => {
): Promise<void> => {
const { title, body } = values;
try {
const newRev = revNum + 1;
const nodes = editPost(newRev, noteId, title, body);
await api.graph.addNodes(ship, book, nodes);
await waiter(p => {
await waiter((p) => {
const [rev] = getLatestRevision(p.note);
return rev === newRev;
});
@ -44,7 +48,7 @@ export function EditPost(props: EditPostProps & RouteComponentProps) {
history.push(noteUrl);
} catch (e) {
console.error(e);
actions.setStatus({ error: "Failed to edit notebook" });
actions.setStatus({ error: 'Failed to edit notebook' });
}
};

View File

@ -1,26 +1,26 @@
import React, { createRef, useCallback, useRef } from "react";
import { IUnControlledCodeMirror, UnControlled as CodeEditor } from "react-codemirror2";
import React, { createRef, useCallback, useRef } from 'react';
import { IUnControlledCodeMirror, UnControlled as CodeEditor } from 'react-codemirror2';
import { useFormikContext } from 'formik';
import { Prompt } from 'react-router-dom';
import { Editor } from 'codemirror';
import { MOBILE_BROWSER_REGEX, usePreventWindowUnload } from "~/logic/lib/util";
import { PropFunc } from "~/types/util";
import CodeMirror from "codemirror";
import { MOBILE_BROWSER_REGEX, usePreventWindowUnload } from '~/logic/lib/util';
import { PropFunc } from '~/types/util';
import CodeMirror from 'codemirror';
import "codemirror/mode/markdown/markdown";
import "codemirror/addon/display/placeholder";
import "codemirror/addon/edit/continuelist";
import 'codemirror/mode/markdown/markdown';
import 'codemirror/addon/display/placeholder';
import 'codemirror/addon/edit/continuelist';
import "codemirror/lib/codemirror.css";
import { Box } from "@tlon/indigo-react";
import { useFileDrag } from "~/logic/lib/useDrag";
import SubmitDragger from "~/views/components/SubmitDragger";
import useS3 from "~/logic/lib/useS3";
import { S3State } from "~/types";
import 'codemirror/lib/codemirror.css';
import { Box } from '@tlon/indigo-react';
import { useFileDrag } from '~/logic/lib/useDrag';
import SubmitDragger from '~/views/components/SubmitDragger';
import useS3 from '~/logic/lib/useS3';
import { S3State } from '@urbit/api';
const MARKDOWN_CONFIG = {
name: "markdown",
name: 'markdown'
};
interface MarkdownEditorProps {
@ -49,12 +49,12 @@ export function MarkdownEditor(
const options = {
mode: MARKDOWN_CONFIG,
theme: "tlon",
theme: 'tlon',
lineNumbers: false,
lineWrapping: true,
scrollbarStyle: "native",
scrollbarStyle: 'native',
// cursorHeight: 0.85,
placeholder: placeholder || "",
placeholder: placeholder || '',
extraKeys: { 'Enter': 'newlineAndIndentContinueMarkdownList' }
};
@ -84,7 +84,7 @@ export function MarkdownEditor(
const codeMirror: Editor = editor.current.editor;
const doc = codeMirror.getDoc();
Array.from(files).forEach(async file => {
Array.from(files).forEach(async (file) => {
const placeholder = `![Uploading ${file.name}](...)`;
doc.setValue(doc.getValue() + placeholder);
const url = await uploadDefault(file);

View File

@ -1,8 +1,8 @@
import React, { useCallback } from "react";
import _ from "lodash";
import { Box, ErrorLabel } from "@tlon/indigo-react";
import { useField } from "formik";
import { MarkdownEditor } from "./MarkdownEditor";
import React, { useCallback } from 'react';
import _ from 'lodash';
import { Box, ErrorLabel } from '@tlon/indigo-react';
import { useField } from 'formik';
import { MarkdownEditor } from './MarkdownEditor';
export const MarkdownField = ({
id,
@ -13,13 +13,13 @@ export const MarkdownField = ({
const handleBlur = useCallback(
(e: any) => {
_.set(e, "target.id", id);
_.set(e, 'target.id', id);
onBlur && onBlur(e);
},
[onBlur, id]
);
const hasError = !!(error && touched);
const hasError = Boolean(error && touched);
return (
<Box
@ -31,13 +31,13 @@ export const MarkdownField = ({
{...rest}
>
<MarkdownEditor
borderColor={hasError ? "red" : "lightGray"}
borderColor={hasError ? 'red' : 'lightGray'}
onBlur={handleBlur}
value={value}
onChange={setValue}
s3={s3}
/>
<ErrorLabel mt="2" hasError={!!(error && touched)}>
<ErrorLabel mt="2" hasError={Boolean(error && touched)}>
{error}
</ErrorLabel>
</Box>

View File

@ -12,10 +12,10 @@ import {
import { Formik, Form, useFormikContext, FormikHelpers } from "formik";
import GlobalApi from "~/logic/api/global";
import { Notebook } from "~/types/publish-update";
import { Contacts } from "~/types/contact-update";
import { Contacts } from "@urbit/api/contacts";
import { FormError } from "~/views/components/FormError";
import { RouteComponentProps, useHistory } from "react-router-dom";
import {Association} from "~/types";
import {Association} from "@urbit/api";
import { uxToHex } from "~/logic/lib/util";
interface MetadataFormProps {

View File

@ -1,16 +1,16 @@
import React, { useState, useEffect } from "react";
import { Box, Text, Col } from "@tlon/indigo-react";
import ReactMarkdown from "react-markdown";
import React, { useState, useEffect } from 'react';
import { Box, Text, Col } from '@tlon/indigo-react';
import ReactMarkdown from 'react-markdown';
import bigInt from 'big-integer';
import { Link, RouteComponentProps } from "react-router-dom";
import { Spinner } from "~/views/components/Spinner";
import { Comments } from "~/views/components/Comments";
import { NoteNavigation } from "./NoteNavigation";
import GlobalApi from "~/logic/api/global";
import { Link, RouteComponentProps } from 'react-router-dom';
import { Spinner } from '~/views/components/Spinner';
import { Comments } from '~/views/components/Comments';
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, Association, Unreads, Group } from "~/types";
import Author from '~/views/components/Author';
import { Contacts, GraphNode, Graph, Association, Unreads, Group } from '@urbit/api';
interface NoteProps {
ship: string;
@ -34,12 +34,11 @@ export function Note(props: NoteProps & RouteComponentProps) {
const deletePost = async () => {
setDeleting(true);
const indices = [note.post.index]
const indices = [note.post.index];
await api.graph.removeNodes(ship, book, indices);
props.history.push(rootUrl);
};
const comments = getComments(note);
const [revNum, title, body, post] = getLatestRevision(note);
const index = note.post.index.split('/');
@ -49,8 +48,6 @@ export function Note(props: NoteProps & RouteComponentProps) {
api.hark.markEachAsRead(props.association, '/',`/${index[1]}/1/1`, 'note', 'publish');
}, [props.association, props.note]);
let adminLinks: JSX.Element | null = null;
if (window.ship === note?.post?.author) {
adminLinks = (
@ -67,7 +64,7 @@ export function Note(props: NoteProps & RouteComponentProps) {
color="red"
ml={2}
onClick={deletePost}
style={{ cursor: "pointer" }}
style={{ cursor: 'pointer' }}
>
Delete
</Text>
@ -96,21 +93,21 @@ export function Note(props: NoteProps & RouteComponentProps) {
ref={windowRef}
>
<Link to={rootUrl}>
<Text>{"<- Notebook Index"}</Text>
<Text>{'<- Notebook Index'}</Text>
</Link>
<Col>
<Text display="block" mb={2}>{title || ""}</Text>
<Text display="block" mb={2}>{title || ''}</Text>
<Box display="flex">
<Author
ship={post?.author}
contacts={contacts}
date={post?.["time-sent"]}
date={post?.['time-sent']}
/>
<Text ml={2}>{adminLinks}</Text>
</Box>
</Col>
<Box color="black" className="md" style={{ overflowWrap: "break-word", overflow: "hidden" }}>
<ReactMarkdown source={body} linkTarget={"_blank"} />
<Box color="black" className="md" style={{ overflowWrap: 'break-word', overflow: 'hidden' }}>
<ReactMarkdown source={body} linkTarget={'_blank'} />
</Box>
<NoteNavigation
notebook={notebook}

View File

@ -5,11 +5,11 @@ import {
Row,
Col,
Button
} from "@tlon/indigo-react";
import { AsyncButton } from "../../../components/AsyncButton";
import { Formik, Form, FormikHelpers } from "formik";
import { MarkdownField } from "./MarkdownField";
import { S3State } from "~/types";
} from '@tlon/indigo-react';
import { AsyncButton } from '../../../components/AsyncButton';
import { Formik, Form, FormikHelpers } from 'formik';
import { MarkdownField } from './MarkdownField';
import { S3State } from '@urbit/api';
interface PostFormProps {
initial: PostFormSchema;
@ -63,7 +63,8 @@ export function PostForm(props: PostFormProps) {
onClick={() => {
history.goBack();
}}
type="button">Cancel</Button>}
type="button"
>Cancel</Button>}
</Row>
</Row>
<MarkdownField flexGrow={1} id="body" s3={s3} />

View File

@ -1,29 +1,31 @@
import React, { Component } from "react";
import moment from "moment";
import { Box } from "@tlon/indigo-react";
import { Link } from "react-router-dom";
import { Graph, GraphNode } from "~/types";
import { getLatestRevision } from "~/logic/lib/publish";
import { BigInteger } from "big-integer";
import React, { ReactElement } from 'react';
import moment from 'moment';
import { Link } from 'react-router-dom';
import { BigInteger } from 'big-integer';
import { Box } from '@tlon/indigo-react';
import { Graph } from '@urbit/api';
import { getLatestRevision } from '~/logic/lib/publish';
function NavigationItem(props: {
url: string;
title: string;
date: number;
prev?: boolean;
}) {
}): ReactElement {
const date = moment(props.date).fromNow();
return (
<Box
justifySelf={props.prev ? "start" : "end"}
justifySelf={props.prev ? 'start' : 'end'}
display="flex"
flexDirection="column"
justifyContent="flex-end"
textAlign={props.prev ? "left" : "right"}
textAlign={props.prev ? 'left' : 'right'}
>
<Link to={props.url}>
<Box color="gray" mb={2}>
{props.prev ? "Previous" : "Next"}
{props.prev ? 'Previous' : 'Next'}
</Box>
<Box mb={1}>{props.title}</Box>
<Box color="gray">{date}</Box>
@ -53,7 +55,7 @@ interface NoteNavigationProps {
baseUrl: string;
}
export function NoteNavigation(props: NoteNavigationProps) {
export function NoteNavigation(props: NoteNavigationProps): ReactElement {
let nextComponent = <Box />;
let prevComponent = <Box />;
const { noteId, notebook } = props;
@ -72,13 +74,13 @@ export function NoteNavigation(props: NoteNavigationProps) {
if (next && nextId) {
const nextUrl = makeNoteUrl(nextId);
const [, title, , post] = getLatestRevision(next);
const date = post["time-sent"];
const date = post['time-sent'];
nextComponent = <NavigationItem title={title} date={date} url={nextUrl} />;
}
if (prev && prevId) {
const prevUrl = makeNoteUrl(prevId);
const [, title, , post] = getLatestRevision(prev);
const date = post["time-sent"];
const date = post['time-sent'];
prevComponent = (
<NavigationItem title={title} date={date} url={prevUrl} prev />
);

Some files were not shown because too many files have changed in this diff Show More