Merge remote-tracking branch 'origin/release/next-js' into lf/tutorial-revive

This commit is contained in:
Liam Fitzgerald 2021-03-02 13:11:43 +10:00
commit 38968bde94
No known key found for this signature in database
GPG Key ID: D390E12C61D1CFFB
109 changed files with 2984 additions and 1969 deletions

View File

@ -15,3 +15,13 @@ jobs:
target_branch: release/next-js
github_token: ${{ secrets.JANEWAY_BOT_TOKEN }}
merge-to-group-timer:
runs-on: ubuntu-latest
name: "Merge master to ops/group-timer"
steps:
- uses: actions/checkout@v2
- uses: devmasx/merge-branch@v1.3.1
with:
type: now
target_branch: ops/group-timer
github_token: ${{ secrets.JANEWAY_BOT_TOKEN }}

20
.github/workflows/ops-group-timer.yml vendored Normal file
View File

@ -0,0 +1,20 @@
name: group-timer
on:
push:
branches:
- 'ops/group-timer'
jobs:
glob:
runs-on: ubuntu-latest
name: "Create and deploy a glob to ~difmex-passed"
steps:
- uses: actions/checkout@v2
with:
lfs: true
- uses: ./.github/actions/glob
with:
ship: 'difmex-passed'
credentials: ${{ secrets.JANEWAY_SERVICE_KEY }}
ssh-sec-key: ${{ secrets.JANEWAY_SSH_SEC_KEY }}
ssh-pub-key: ${{ secrets.JANEWAY_SSH_PUB_KEY }}

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:7bf07220286010c21998e38497be5f2c771219bbcb6dac3e2c7bb0af5dbf4fb8
size 9668893
oid sha256:9812a52d34be0d6d47ca60b23d3386e7db296ff61fac7c4b1f33a35806f8cb7c
size 9751012

View File

@ -5,7 +5,7 @@
/- glob
/+ default-agent, verb, dbug
|%
++ hash 0v3.10f5l.mmsef.76usq.9a3gk.0rmog
++ hash 0v5.ip41o.9jcdb.4jb1f.sd508.fdssj
+$ state-0 [%0 hash=@uv glob=(unit (each glob:glob tid=@ta))]
+$ all-states
$% state-0

View File

@ -767,6 +767,8 @@
++ inflate-cache
|= state-4
^+ +.state
=. +.state
*cache
=/ nots=(list [p=@da =timebox:store])
(tap:orm notifications)
|- =* outer $

View File

@ -24,6 +24,6 @@
<div id="portal-root"></div>
<script src="/~landscape/js/channel.js"></script>
<script src="/~landscape/js/session.js"></script>
<script src="/~landscape/js/bundle/index.27d9bb22f3eebe7228b8.js"></script>
<script src="/~landscape/js/bundle/index.01423cd5af57c2f23adc.js"></script>
</body>
</html>

View File

@ -6,16 +6,8 @@
$% state-0
state-1
==
+$ state-0
$: %0
=settings
==
::
+$ state-1
$: %1
=settings
==
+$ state-0 [%0 settings=settings-0]
+$ state-1 [%1 =settings]
--
=| state-1
=* state -
@ -43,19 +35,10 @@
|= =old=vase
^- (quip card _this)
=/ old !<(versioned-state old-vase)
=| cards=(list card)
|-
?- -.old
%1 [cards this(state old)]
::
%0
%_ $
-.old %1
::
cards
:_ cards
(poke-self:pass:io settngs-event+!>([%put-entry %tutorial %seen b+|]))
==
%0 $(old [%1 +.old])
%1 [~ this(state old)]
==
::
++ on-poke

View File

@ -50,7 +50,7 @@
%- pairs
:~ bucket-key+s+b
entry-key+s+k
value+(val v)
value+(value v)
==
::
++ del-entry
@ -68,6 +68,7 @@
%s val
%b val
%n (numb p.val)
%a [%a (turn p.val value)]
==
::
++ bucket
@ -105,7 +106,7 @@
%- ot
:~ bucket-key+so
entry-key+so
value+val
value+value
==
::
++ del-entry
@ -121,6 +122,7 @@
%s jon
%b jon
%n [%n (rash p.jon dem)]
%a [%a (turn p.jon value)]
==
::
++ bucket

View File

@ -1,11 +1,21 @@
|%
+$ settings-0 (map key bucket-0)
+$ bucket-0 (map key val-0)
+$ val-0
$% [%s p=@t]
[%b p=?]
[%n p=@]
==
::
+$ settings (map key bucket)
+$ bucket (map key val)
+$ key term
+$ val
$~ [%n 0]
$% [%s p=@t]
[%b p=?]
[%n p=@]
[%a p=(list val)]
==
+$ event
$% [%put-bucket =key =bucket]

View File

@ -1217,7 +1217,7 @@
on-hear-forward
::
?: ?& ?=(%pawn (clan:title sndr.packet))
!(~(has by peers.ames-state) sndr.packet)
!?=([~ %known *] (~(get by peers.ames-state) sndr.packet))
==
on-hear-open
on-hear-shut
@ -1289,14 +1289,9 @@
|= [=lane =packet dud=(unit goof)]
^+ event-core
=/ sndr-state (~(get by peers.ames-state) sndr.packet)
:: if we don't know them, maybe enqueue a jael %public-keys request
::
:: Ignore encrypted packets from alien comets.
:: TODO: maybe crash?
:: if we don't know them, ask jael for their keys and enqueue
::
?. ?=([~ %known *] sndr-state)
?: =(%pawn (clan:title sndr.packet))
event-core
(enqueue-alien-todo sndr.packet |=(alien-agenda +<))
:: decrypt packet contents using symmetric-key.channel
::

Binary file not shown.

View File

@ -95,7 +95,7 @@
"tsc": "tsc",
"tsc:watch": "tsc --watch",
"build:dev": "cross-env NODE_ENV=development webpack --config config/webpack.dev.js",
"build:prod": "cross-env NODE_ENV=production webpack --config config/webpack.prod.js",
"build:prod": "cd ../npm/api && npm i && cd ../../interface && cross-env NODE_ENV=production webpack --config config/webpack.prod.js",
"start": "webpack-dev-server --config config/webpack.dev.js",
"test": "echo \"Error: no test specified\" && exit 1"
},

View File

@ -11,7 +11,7 @@ export default class SettingsApi extends BaseApi<StoreState> {
}
putBucket(key: Key, bucket: Bucket) {
this.storeAction({
return this.storeAction({
'put-bucket': {
'bucket-key': key,
'bucket': bucket
@ -20,7 +20,7 @@ export default class SettingsApi extends BaseApi<StoreState> {
}
delBucket(key: Key) {
this.storeAction({
return this.storeAction({
'del-bucket': {
'bucket-key': key
}
@ -38,7 +38,7 @@ export default class SettingsApi extends BaseApi<StoreState> {
}
delEntry(buc: Key, key: Key) {
this.storeAction({
return this.storeAction({
'put-entry': {
'bucket-key': buc,
'entry-key': key
@ -47,8 +47,10 @@ export default class SettingsApi extends BaseApi<StoreState> {
}
async getAll() {
const data = await this.scry('settings-store', '/all');
this.store.handleEvent({ data: { 'settings-data': data.all } });
const { all } = await this.scry("settings-store", "/all");
this.store.handleEvent({data:
{"settings-data": { all } }
});
}
async getBucket(bucket: Key) {

View File

@ -0,0 +1,72 @@
import useLocalState, { LocalState } from "~/logic/state/local";
import useSettingsState from "~/logic/state/settings";
import GlobalApi from "../api/global";
import { BackgroundConfig, RemoteContentPolicy } from "~/types";
const getBackgroundString = (bg: BackgroundConfig) => {
if (bg?.type === "url") {
return bg.url;
} else if (bg?.type === "color") {
return bg.color;
} else {
return "";
}
};
export function useMigrateSettings(api: GlobalApi) {
const local = useLocalState();
const { display, remoteContentPolicy, calm } = useSettingsState();
return async () => {
if (!localStorage?.has("localReducer")) {
return;
}
let promises: Promise<any>[] = [];
if (local.hideAvatars !== calm.hideAvatars) {
promises.push(
api.settings.putEntry("calm", "hideAvatars", local.hideAvatars)
);
}
if (local.hideNicknames !== calm.hideNicknames) {
promises.push(
api.settings.putEntry("calm", "hideNicknames", local.hideNicknames)
);
}
if (
local?.background?.type &&
display.background !== getBackgroundString(local.background)
) {
promises.push(
api.settings.putEntry(
"display",
"background",
getBackgroundString(local.background)
)
);
promises.push(
api.settings.putEntry(
"display",
"backgroundType",
local.background?.type
)
);
}
Object.keys(local.remoteContentPolicy).forEach((_key) => {
const key = _key as keyof RemoteContentPolicy;
const localVal = local.remoteContentPolicy[key];
if (localVal !== remoteContentPolicy[key]) {
promises.push(
api.settings.putEntry("remoteContentPolicy", key, localVal)
);
}
});
await Promise.all(promises);
localStorage.removeItem("localReducer");
};
}

View File

@ -1,7 +1,7 @@
import { cite } from '~/logic/lib/util';
import { isChannelAdmin } from '~/logic/lib/group';
const indexes = new Map([
const makeIndexes = () => new Map([
['ships', []],
['commands', []],
['subscriptions', []],
@ -70,19 +70,27 @@ const appIndex = function (apps) {
return applications;
};
const otherIndex = function() {
const otherIndex = function(config) {
const other = [];
other.push(result('My Channels', '/~landscape/home', 'home', null));
const idx = {
mychannel: result('My Channels', '/~landscape/home', 'home', null),
updates: result('Notifications', '/~notifications', 'inbox', null),
profile: result('Profile', `/~profile/~${window.ship}`, 'profile', null),
messages: result('Messages', '/~landscape/messages', 'messages', null),
logout: result('Log Out', '/~/logout', 'logout', null)
};
other.push(result('Tutorial', '/?tutorial=true', 'tutorial', null));
other.push(result('Notifications', '/~notifications', 'inbox', null));
other.push(result('Profile and Settings', `/~profile/~${window.ship}`, 'profile', null));
other.push(result('Messages', '/~landscape/messages', 'messages', null));
other.push(result('Log Out', '/~/logout', 'logout', null));
for(let cat of config.categories) {
if(idx[cat]) {
other.push(idx[cat]);
}
}
return other;
};
export default function index(contacts, associations, apps, currentGroup, groups) {
export default function index(contacts, associations, apps, currentGroup, groups, hide) {
const indexes = makeIndexes();
indexes.set('ships', shipIndex(contacts));
// all metadata from all apps is indexed
// into subscriptions and landscape
@ -142,7 +150,7 @@ export default function index(contacts, associations, apps, currentGroup, groups
indexes.set('subscriptions', subscriptions);
indexes.set('groups', landscape);
indexes.set('apps', appIndex(apps));
indexes.set('other', otherIndex());
indexes.set('other', otherIndex(hide));
return indexes;
};

View File

@ -1,6 +1,6 @@
import urbitOb from 'urbit-ob';
const URL_REGEX = new RegExp(String(/^((\w+:\/\/)[-a-zA-Z0-9:@;?&=\/%\+\.\*!'\(\),\$_\{\}\^~\[\]`#|]+)/.source));
const URL_REGEX = new RegExp(String(/^((\w+:\/\/)[-a-zA-Z0-9:@;?&=\/%\+\.\*!'\(\),\$_\{\}\^~\[\]`#|]+\w)/.source));
const isUrl = (string) => {
try {
@ -52,16 +52,13 @@ const tokenizeMessage = (text) => {
}
messages.push({ url: str });
message = [];
} else if (urbitOb.isValidPatp(str.replace(/[^a-z\-\~]/g, '')) && !isInCodeBlock) {
} else if(urbitOb.isValidPatp(str) && !isInCodeBlock) {
if (message.length > 0) {
// If we're in the middle of a message, add it to the stack and reset
messages.push({ text: message.join(' ') });
message = [];
}
messages.push({ mention: str.replace(/[^a-z\-\~]/g, '') });
if (str.replace(/[a-z\-\~]/g, '').length > 0) {
messages.push({ text: str.replace(/[a-z\-\~]/g, '') });
}
messages.push({ mention: str });
message = [];
} else {

View File

@ -1,9 +1,9 @@
import { useEffect, useState } from 'react';
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';
import _ from "lodash";
import f, { memoize } from "lodash/fp";
import bigInt, { BigInteger } from "big-integer";
import { Contact } from '~/types';
import useSettingsState from '../state/settings';
export const MOBILE_BROWSER_REGEX = /Android|webOS|iPhone|iPad|iPod|BlackBerry/i;
@ -376,8 +376,8 @@ 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 Boolean(contact && contact.nickname && !hideNicknames);
const hideNicknames = typeof hide !== 'undefined' ? hide : useSettingsState(state => state.calm.hideNicknames);
return !!(contact && contact.nickname && !hideNicknames);
}
interface useHoveringInterface {

View File

@ -1,77 +1,83 @@
import _ from 'lodash';
import { StoreState } from '../../store/type';
import {
SettingsUpdate
} from '@urbit/api/settings';
import { SettingsUpdate } from '~/types/settings';
import useSettingsState, { SettingsStateZus } from "~/logic/state/settings";
import produce from 'immer';
type SettingsState = Pick<StoreState, 'settings'>;
export default class SettingsReducer<S extends SettingsState> {
reduce(json: Cage, state: S) {
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'];
if (data) {
this.getAll(data, state);
this.getBucket(data, state);
this.getEntry(data, state);
}
export default class SettingsStateZusettingsReducer{
reduce(json: any) {
const old = useSettingsState.getState();
const newState = produce(old, state => {
let data = json["settings-event"];
if (data) {
console.log(data);
this.putBucket(data, state);
this.delBucket(data, state);
this.putEntry(data, state);
this.delEntry(data, state);
}
data = json["settings-data"];
if (data) {
console.log(data);
this.getAll(data, state);
this.getBucket(data, state);
this.getEntry(data, state);
}
});
useSettingsState.setState(newState);
}
putBucket(json: SettingsUpdate, state: S) {
putBucket(json: SettingsUpdate, state: SettingsStateZus) {
const data = _.get(json, 'put-bucket', false);
if (data) {
state.settings[data['bucket-key']] = data.bucket;
state[data["bucket-key"]] = data.bucket;
}
}
delBucket(json: SettingsUpdate, state: S) {
delBucket(json: SettingsUpdate, state: SettingsStateZus) {
const data = _.get(json, 'del-bucket', false);
if (data) {
delete state.settings[data['bucket-key']];
delete settings[data['bucket-key']];
}
}
putEntry(json: SettingsUpdate, state: S) {
putEntry(json: SettingsUpdate, state: SettingsStateZus) {
const data = _.get(json, 'put-entry', false);
if (data) {
if (!state.settings[data['bucket-key']]) {
state.settings[data['bucket-key']] = {};
if (!state[data["bucket-key"]]) {
state[data["bucket-key"]] = {};
}
state.settings[data['bucket-key']][data['entry-key']] = data.value;
state[data["bucket-key"]][data["entry-key"]] = data.value;
}
}
delEntry(json: SettingsUpdate, state: S) {
delEntry(json: SettingsUpdate, state: SettingsStateZus) {
const data = _.get(json, 'del-entry', false);
if (data) {
delete state.settings[data['bucket-key']][data['entry-key']];
delete state[data["bucket-key"]][data["entry-key"]];
}
}
getAll(json: any, state: S) {
state.settings = json;
getAll(json: any, state: SettingsStateZus) {
const data = _.get(json, 'all');
if(data) {
_.merge(state, data);
}
}
getBucket(json: any, state: S) {
getBucket(json: any, state: SettingsStateZus) {
const key = _.get(json, 'bucket-key', false);
const bucket = _.get(json, 'bucket', false);
if (key && bucket) {
state.settings[key] = bucket;
state[key] = bucket;
}
}
getEntry(json: any, state: S) {
getEntry(json: any, state: SettingsStateZus) {
const bucketKey = _.get(json, 'bucket-key', false);
const entryKey = _.get(json, 'entry-key', false);
const entry = _.get(json, 'entry', false);
if (bucketKey && entryKey && entry) {
state.settings[bucketKey][entryKey] = entry;
state[bucketKey][entryKey] = entry;
}
}
}

View File

@ -3,17 +3,20 @@ 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, LeapCategories } from "~/types/local-update";
export interface LocalState extends State {
export interface LocalState {
hideAvatars: boolean;
hideNicknames: boolean;
remoteContentPolicy: RemoteContentPolicy;
tutorialProgress: TutorialProgress;
hideGroups: boolean;
tutorialRef: HTMLElement | null,
hideTutorial: () => void;
nextTutStep: () => void;
prevTutStep: () => void;
hideLeapCats: LeapCategories[];
setTutorialRef: (el: HTMLElement | null) => void;
dark: boolean;
background: BackgroundConfig;
@ -21,15 +24,20 @@ export interface LocalState extends State {
suspendedFocus?: HTMLElement;
toggleOmnibox: () => void;
set: (fn: (state: LocalState) => void) => void
}
export const selectLocalState =
};
type LocalStateZus = LocalState & State;
export const selectLocalState =
<K extends keyof LocalState>(keys: K[]) => f.pick<LocalState, K>(keys);
const useLocalState = create<LocalState>(persist((set, get) => ({
const useLocalState = create<LocalStateZus>(persist((set, get) => ({
dark: false,
background: undefined,
hideAvatars: false,
hideNicknames: false,
hideLeapCats: [],
hideGroups: false,
tutorialProgress: 'hidden',
tutorialRef: null,
setTutorialRef: (el: HTMLElement | null) => set(produce((state) => {

View File

@ -0,0 +1,68 @@
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, LeapCategories, leapCategories } from "~/types/local-update";
export interface SettingsState {
display: {
backgroundType: 'none' | 'url' | 'color';
background?: string;
dark: boolean;
};
calm: {
hideNicknames: boolean;
hideAvatars: boolean;
hideUnreads: boolean;
hideGroups: boolean;
};
remoteContentPolicy: RemoteContentPolicy;
leap: {
categories: LeapCategories[];
}
set: (fn: (state: SettingsState) => void) => void
};
export type SettingsStateZus = SettingsState & State;
export const selectSettingsState =
<K extends keyof SettingsState>(keys: K[]) => f.pick<SettingsState, K>(keys);
export const selectCalmState = (s: SettingsState) => s.calm;
const useSettingsState = create<SettingsStateZus>((set) => ({
display: {
backgroundType: 'none',
background: undefined,
dark: false,
},
calm: {
hideNicknames: false,
hideAvatars: false,
hideUnreads: false,
hideGroups: false
},
remoteContentPolicy: {
imageShown: true,
oembedShown: true,
audioShown: true,
videoShown: true
},
leap: {
categories: leapCategories,
},
set: (fn: (state: SettingsState) => void) => set(produce(fn))
}));
function withSettingsState<P, S extends keyof SettingsState>(Component: any, stateMemberKeys?: S[]) {
return React.forwardRef((props: Omit<P, S>, ref) => {
const localState = stateMemberKeys
? useSettingsState(selectSettingsState(stateMemberKeys))
: useSettingsState();
return <Component ref={ref} {...localState} {...props} />
});
}
export { useSettingsState as default, withSettingsState };

View File

@ -114,7 +114,7 @@ export default class GlobalStore extends BaseStore<StoreState> {
GraphReducer(data, this.state);
HarkReducer(data, this.state);
ContactReducer(data, this.state);
this.settingsReducer.reduce(data, this.state);
this.settingsReducer.reduce(data);
GroupViewReducer(data, this.state);
}
}

View File

@ -1,5 +1,9 @@
export const tutorialProgress = ['hidden', 'start', 'group-desc', 'channels', 'chat', 'link', 'publish', 'profile', 'leap', 'notifications', 'done', 'exit'] as const;
export const leapCategories = ["mychannel", "messages", "updates", "profile", "logout"] as const;
export type LeapCategories = typeof leapCategories[number];
export type TutorialProgress = typeof tutorialProgress[number];
interface LocalUpdateSetDark {
setDark: boolean;

View File

@ -28,19 +28,20 @@ import GlobalApi from '~/logic/api/global';
import { uxToHex } from '~/logic/lib/util';
import { foregroundFromBackground } from '~/logic/lib/sigil';
import { withLocalState } from '~/logic/state/local';
import { withSettingsState } from '~/logic/state/settings';
const Root = styled.div`
const Root = withSettingsState(styled.div`
font-family: ${p => p.theme.fonts.sans};
height: 100%;
width: 100%;
padding: 0;
margin: 0;
${p => p.background?.type === 'url' ? `
background-image: url('${p.background?.url}');
${p => p.display.backgroundType === 'url' ? `
background-image: url('${p.display.background}');
background-size: cover;
` : p.background?.type === 'color' ? `
background-color: ${p.background.color};
` : p.display.backgroundType === 'color' ? `
background-color: ${p.display.background};
` : `background-color: ${p.theme.colors.white};`
}
display: flex;
@ -64,7 +65,7 @@ const Root = styled.div`
border-radius: 1rem;
border: 0px solid transparent;
}
`;
`, ['display']);
const StatusBarWithRouter = withRouter(StatusBar);
@ -148,7 +149,7 @@ class App extends React.Component {
? <link rel="icon" type="image/svg+xml" href={this.faviconString()} />
: null}
</Helmet>
<Root background={background}>
<Root>
<Router>
<TutorialModal api={this.api} />
<ErrorBoundary>

View File

@ -16,6 +16,7 @@ import {
cite,
writeText,
useShowNickname,
useHideAvatar,
useHovering
} from '~/logic/lib/util';
import {
@ -32,6 +33,7 @@ import RemoteContent from '~/views/components/RemoteContent';
import { Mention } from '~/views/components/MentionText';
import styled from 'styled-components';
import useLocalState from '~/logic/state/local';
import useSettingsState, {selectCalmState} from "~/logic/state/settings";
import Timestamp from '~/views/components/Timestamp';
export const DATESTAMP_FORMAT = '[~]YYYY.M.D';
@ -238,7 +240,7 @@ export const MessageAuthor = ({
const contact =
`~${msg.author}` in contacts ? contacts[`~${msg.author}`] : false;
const showNickname = useShowNickname(contact);
const { hideAvatars } = useLocalState(({ hideAvatars }) => ({ hideAvatars }));
const { hideAvatars } = useSettingsState(selectCalmState);
const shipName = showNickname ? contact.nickname : cite(msg.author);
const copyNotice = 'Copied';
const color = contact

View File

@ -30,6 +30,8 @@ import {
TUTORIAL_CHAT,
TUTORIAL_LINKS
} from '~/logic/lib/tutorialModal';
import useSettingsState, { selectCalmState } from '~/logic/state/settings';
const ScrollbarLessBox = styled(Box)`
scrollbar-width: none !important;
@ -39,7 +41,7 @@ const ScrollbarLessBox = styled(Box)`
}
`;
const tutSelector = f.pick(['tutorialProgress', 'nextTutStep']);
const tutSelector = f.pick(['tutorialProgress', 'nextTutStep', 'hideGroups']);
export default function LaunchApp(props) {
const history = useHistory();
@ -84,6 +86,8 @@ export default function LaunchApp(props) {
}, [query]);
const { tutorialProgress, nextTutStep } = useLocalState(tutSelector);
let { hideGroups } = useLocalState(tutSelector);
!hideGroups ? { hideGroups } = useSettingsState(selectCalmState) : null;
const waiter = useWaitForProps(props);
@ -211,8 +215,9 @@ export default function LaunchApp(props) {
>
<JoinGroup {...props} />
</ModalButton>
<Groups unreads={props.unreads} groups={props.groups} associations={props.associations} />
{!hideGroups &&
(<Groups unreads={props.unreads} groups={props.groups} associations={props.associations} />)
}
</Box>
<Box alignSelf="flex-start" display={["block", "none"]}>{hashBox}</Box>
</ScrollbarLessBox>

View File

@ -3,12 +3,13 @@ import { Box, Text, Col } from '@tlon/indigo-react';
import f from 'lodash/fp';
import _ from 'lodash';
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_GROUP_RESOURCE, 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, TUTORIAL_GROUP_RESOURCE } from '~/logic/lib/tutorialModal';
import useSettingsState, { selectCalmState } from '~/logic/state/settings';
interface GroupsProps {
associations: Associations;
@ -84,11 +85,12 @@ function Group(props: GroupProps) {
isTutorialGroup,
anchorRef
);
const { hideUnreads } = useSettingsState(selectCalmState)
return (
<Tile ref={anchorRef} position="relative" bg={isTutorialGroup ? 'lightBlue' : undefined} to={`/~landscape${path}`} gridColumnStart={first ? '1' : null}>
<Col height="100%" justifyContent="space-between">
<Text>{title}</Text>
<Col>
{!hideUnreads && (<Col>
{updates > 0 &&
(<Text mt="1" color="blue">{updates} update{updates !== 1 && 's'} </Text>)
}
@ -96,7 +98,7 @@ function Group(props: GroupProps) {
(<Text color="lightGray">{unreads}</Text>)
}
</Col>
)}
</Col>
</Tile>
);

View File

@ -67,6 +67,7 @@ export const LinkItem = (props: LinkItemProps): ReactElement => {
const size = node.children ? node.children.size : 0;
const contents = node.post.contents;
const hostname = URLparser.exec(contents[1].url) ? URLparser.exec(contents[1].url)[4] : null;
const href = URLparser.exec(contents[1].url) ? contents[1].url : `http://${contents[1].url}`
const baseUrl = props.baseUrl || `/~404/${resource}`;
@ -120,7 +121,7 @@ export const LinkItem = (props: LinkItemProps): ReactElement => {
<RemoteContent
ref={r => { remoteRef.current = r }}
renderUrl={false}
url={contents[1].url}
url={href}
text={contents[0].text}
unfold={true}
onLoad={onMeasure}
@ -145,7 +146,7 @@ export const LinkItem = (props: LinkItemProps): ReactElement => {
}}
/>
<Text color="gray" p={2} flexShrink={0}>
<Anchor target="_blank" rel="noopener noreferrer" style={{ textDecoration: 'none' }} href={contents[1].url}>
<Anchor target="_blank" rel="noopener noreferrer" style={{ textDecoration: 'none' }} href={href}>
<Box display='flex'>
<Icon icon='ArrowExternal' mr={1} />{hostname}
</Box>

View File

@ -8,7 +8,6 @@ import { Box, Col, Text, Row } from '@tlon/indigo-react';
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';
@ -75,19 +74,8 @@ export default function NotificationsScreen(props: any): ReactElement {
borderBottom="1"
borderBottomColor="washedGray"
>
<Text>Notifications</Text>
<Row>
<Box>
<HeaderLink ref={anchorRef} current={view} view="">
Inbox
</HeaderLink>
</Box>
<Box>
<HeaderLink current={view} view="preferences">
Preferences
</HeaderLink>
</Box>
</Row>
<Text ref={anchorRef}>Updates</Text>
<Row
justifyContent="space-between"
>
@ -137,13 +125,6 @@ export default function NotificationsScreen(props: any): ReactElement {
</Dropdown>
</Row>
</Row>
{view === 'preferences' && (
<NotificationPreferences
graphConfig={props.notificationsGraphConfig}
api={props.api}
dnd={props.doNotDisturb}
/>
)}
{!view && <Inbox {...props} filter={filter.groups} />}
</Col>
</Body>

View File

@ -1,88 +0,0 @@
import React, { ReactElement, useCallback } from 'react';
import { Form, FormikHelpers } from 'formik';
import _ from 'lodash';
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;
dnd: boolean;
watchOnSelf: boolean;
watching: string[];
}
interface NotificationPreferencesProps {
graphConfig: NotificationGraphConfig;
dnd: boolean;
api: GlobalApi;
}
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
};
const onSubmit = useCallback(
async (values: FormSchema, actions: FormikHelpers<FormSchema>) => {
console.log(values);
try {
const promises: Promise<any>[] = [];
if (values.mentions !== graphConfig.mentions) {
promises.push(api.hark.setMentions(values.mentions));
}
if (values.watchOnSelf !== graphConfig.watchOnSelf) {
promises.push(api.hark.setWatchOnSelf(values.watchOnSelf));
}
if (values.dnd !== dnd && !_.isUndefined(values.dnd)) {
promises.push(api.hark.setDoNotDisturb(values.dnd));
}
await Promise.all(promises);
actions.setStatus({ success: null });
actions.resetForm({ values: initialValues });
} catch (e) {
console.error(e);
actions.setStatus({ error: e.message });
}
},
[api, graphConfig]
);
return (
<FormikOnBlur
initialValues={initialValues}
onSubmit={onSubmit}
>
<Form>
<Col maxWidth="384px" p="3" gapY="4">
<Checkbox
label="Do not disturb"
id="dnd"
caption="You won't see the notification badge, but notifications will still appear in your inbox."
/>
<Checkbox
label="Watch for replies"
id="watchOnSelf"
caption="Automatically follow a post for notifications when it's yours"
/>
<Checkbox
label="Watch for mentions"
id="mentions"
caption="Notify me if someone mentions my @p in a channel I've joined"
/>
</Col>
</Form>
</FormikOnBlur>
);
}

View File

@ -10,7 +10,7 @@ import {
} from "@tlon/indigo-react";
import RichText from '~/views/components/RichText'
import useLocalState from "~/logic/state/local";
import useSettingsState, {selectCalmState} from "~/logic/state/settings";
import { Sigil } from '~/logic/lib/sigil';
import { ViewProfile } from './ViewProfile';
import { EditProfile } from './EditProfile';
@ -18,11 +18,11 @@ import { SetStatusBarModal } from '~/views/components/SetStatusBarModal';
import { uxToHex } from '~/logic/lib/util';
import { useTutorialModal } from '~/views/components/useTutorialModal';
export function Profile(props: any): ReactElement {
const { hideAvatars } = useLocalState(({ hideAvatars }) => ({
hideAvatars
}));
const history = useHistory();
const { hideAvatars } = useSettingsState(selectCalmState);
const history = useHistory();
if (!props.ship) {
return null;

View File

@ -8,17 +8,16 @@ import {
Text,
Row,
Col,
} from '@tlon/indigo-react';
} from "@tlon/indigo-react";
import RichText from "~/views/components/RichText";
import {GroupLink} from "~/views/components/GroupLink";
import {lengthOrder} from "~/logic/lib/util";
import useSettingsState, {selectCalmState} from "~/logic/state/settings";
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): ReactElement {
const { hideNicknames } = useLocalState(({ hideNicknames }) => ({
hideNicknames
}));
export function ViewProfile(props: any) {
const history = useHistory();
const { hideNicknames } = useSettingsState(selectCalmState);
const { api, contact, nacked, isPublic, ship, associations, groups } = props;
return (

View File

@ -4,9 +4,10 @@ import Helmet from 'react-helmet';
import { Box } from '@tlon/indigo-react';
import { Profile } from './components/Profile';
import { Profile } from "./components/Profile";
export default function ProfileScreen(props: any) {
const { dark } = props;
return (
<>
<Helmet defer={false}>

View File

@ -0,0 +1,11 @@
import React from 'react';
import { Link } from 'react-router-dom';
import { Text } from '@tlon/indigo-react';
export function BackButton(props: {}) {
return (
<Link to="/~settings">
<Text display={["block", "none"]} fontSize="2" fontWeight="medium">{"<- Back to System Preferences"}</Text>
</Link>
);
}

View File

@ -1,6 +1,8 @@
import React, { ReactElement } from 'react';
import {
Box,
Text,
Row,
Label,
Col,
@ -26,31 +28,38 @@ export function BackgroundPicker({
s3: S3State;
}): ReactElement {
const rowSpace = { my: 0, alignItems: 'center' };
const radioProps = { my: 4, mr: 4, name: 'bgType' };
const colProps = { my: 3, mr: 4, gapY: 1 };
return (
<Col>
<Label mb="2">Landscape Background</Label>
<Label>Landscape Background</Label>
<Row flexWrap="wrap" {...rowSpace}>
<Radio {...radioProps} label="Image" id="url" />
{bgType === 'url' && (
<Col {...colProps}>
<Radio mb="1" name="bgType" label="Image" id="url" />
<Text ml="5" gray>Set an image background</Text>
<ImageInput
ml="3"
ml="5"
api={api}
s3={s3}
id="bgUrl"
placeholder="Drop or upload a file, or paste a link here"
name="bgUrl"
label="URL"
url={bgUrl || ''}
url={bgUrl || ""}
/>
)}
</Col>
</Row>
<Row {...rowSpace}>
<Radio label="Color" id="color" {...radioProps} />
{bgType === 'color' && (
<ColorInput id="bgColor" label="Color" />
)}
<Col {...colProps}>
<Radio mb="1" label="Color" id="color" name="bgType" />
<Text ml="5" gray>Set a hex-based background</Text>
<ColorInput placeholder="FFFFFF" ml="5" id="bgColor" />
</Col>
</Row>
<Radio label="None" id="none" {...radioProps} />
<Radio
my="3"
caption="Your home screen will simply render as its respective day/night mode color"
name="bgType"
label="None"
id="none" />
</Col>
);
}

View File

@ -1,5 +1,5 @@
import React, { ReactElement, useCallback } from 'react';
import { Formik } from 'formik';
import React, { ReactElement, useCallback, useState } from "react";
import { Formik, FormikHelpers } from 'formik';
import {
ManagedTextInputField as Input,
@ -10,8 +10,9 @@ import {
Menu,
MenuButton,
MenuList,
MenuItem
} from '@tlon/indigo-react';
MenuItem,
Row,
} from "@tlon/indigo-react";
import GlobalApi from '~/logic/api/global';
@ -26,9 +27,12 @@ export function BucketList({
}): ReactElement {
const _buckets = Array.from(buckets);
const [adding, setAdding] = useState(false);
const onSubmit = useCallback(
(values: { newBucket: string }) => {
(values: { newBucket: string }, actions: FormikHelpers<any>) => {
api.s3.addBucket(values.newBucket);
actions.resetForm({ values: { newBucket: "" } });
},
[api]
);
@ -67,7 +71,7 @@ export function BucketList({
alignItems="center"
borderRadius={1}
border={1}
borderColor="washedGray"
borderColor="lightGray"
fontSize={1}
pl={2}
mb={2}
@ -91,10 +95,27 @@ export function BucketList({
)}
</Box>
))}
<Input mt="2" label="New Bucket" id="newBucket" />
<Button mt="2" style={{ cursor: 'pointer' }} borderColor="washedGray" type="submit">
Add
</Button>
{adding && (
<Input
placeholder="Enter your new bucket"
mt="2"
label="New Bucket"
id="newBucket"
/>
)}
<Row gapX="3" mt="3">
<Button type="button" onClick={() => setAdding(false)}>
Cancel
</Button>
<Button
width="fit-content"
primary
type={adding ? "submit" : "button"}
onClick={() => setAdding((s) => !s)}
>
{adding ? "Submit" : "Add new bucket"}
</Button>
</Row>
</Form>
</Formik>
);

View File

@ -0,0 +1,138 @@
import React, {useCallback} from "react";
import {
Box,
ManagedToggleSwitchField as Toggle,
Button,
Col,
Text,
} from "@tlon/indigo-react";
import { Formik, Form, FormikHelpers } from "formik";
import * as Yup from "yup";
import { BackButton } from "./BackButton";
import useSettingsState, {selectSettingsState} from "~/logic/state/settings";
import GlobalApi from "~/logic/api/global";
import {AsyncButton} from "~/views/components/AsyncButton";
interface FormSchema {
hideAvatars: boolean;
hideNicknames: boolean;
hideUnreads: boolean;
hideGroups: boolean;
imageShown: boolean;
audioShown: boolean;
oembedShown: boolean;
videoShown: boolean;
}
const settingsSel = selectSettingsState(["calm", "remoteContentPolicy"]);
export function CalmPrefs(props: {
api: GlobalApi;
}) {
const { api } = props;
const {
calm: {
hideAvatars,
hideNicknames,
hideUnreads,
hideGroups
},
remoteContentPolicy: {
imageShown,
videoShown,
oembedShown,
audioShown,
}
} = useSettingsState(settingsSel);
const initialValues: FormSchema = {
hideAvatars,
hideNicknames,
hideUnreads,
hideGroups,
imageShown,
videoShown,
oembedShown,
audioShown,
};
const onSubmit = useCallback(async (v: FormSchema, actions: FormikHelpers<FormSchema>) => {
await Promise.all([
api.settings.putEntry('calm', 'hideAvatars', v.hideAvatars),
api.settings.putEntry('calm', 'hideNicknames', v.hideNicknames),
api.settings.putEntry('calm', 'hideUnreads', v.hideUnreads),
api.settings.putEntry('calm', 'hideGroups', v.hideGroups),
api.settings.putEntry('remoteContentPolicy', 'imageShown', v.imageShown),
api.settings.putEntry('remoteContentPolicy', 'videoShown', v.videoShown),
api.settings.putEntry('remoteContentPolicy', 'audioShown', v.audioShown),
api.settings.putEntry('remoteContentPolicy', 'oembedShown', v.oembedShown),
]);
actions.setStatus({ success: null });
}, [api]);
return (
<Formik initialValues={initialValues} onSubmit={onSubmit}>
<Form>
<Col borderBottom="1" borderBottomColor="washedGray" p="5" pt="4" gapY="5">
<BackButton/>
<Col gapY="1">
<Text color="black" fontSize={2} fontWeight="medium">
CalmEngine
</Text>
<Text gray>
Modulate various elements across Landscape to maximize calmness
</Text>
</Col>
<Text fontWeight="medium">Home screen</Text>
<Toggle
label="Hide unread counts"
id="hideUnreads"
caption="Do not show unread counts on group tiles"
/>
<Toggle
label="Hide group tiles"
id="hideGroups"
caption="Do not show group tiles"
/>
<Text fontWeight="medium">User-set identity</Text>
<Toggle
label="Disable avatars"
id="hideAvatars"
caption="Do not show user-set avatars"
/>
<Toggle
label="Disable nicknames"
id="hideNicknames"
caption="Do not show user-set nicknames"
/>
<Text fontWeight="medium">Remote Content</Text>
<Toggle
label="Load images"
id="imageShown"
caption="Images will be replaced with an inline placeholder that must be clicked to be viewed"
/>
<Toggle
label="Load audio files"
id="audioShown"
caption="Audio content will be replaced with an inline placeholder that must be clicked to be viewed"
/>
<Toggle
label="Load video files"
id="videoShown"
caption="Video content will be replaced with an inline placeholder that must be clicked to be viewed"
/>
<Toggle
label="Load embedded content"
id="oembedShown"
caption="Embedded content may contain scripts that can track you"
/>
<AsyncButton primary width="fit-content" type="submit">
Save
</AsyncButton>
</Col>
</Form>
</Formik>
);
}

View File

@ -1,35 +1,32 @@
import React from 'react';
import React from "react";
import {
Box,
ManagedCheckboxField as Checkbox,
Button
} from '@tlon/indigo-react';
import { Formik, Form } from 'formik';
import * as Yup from 'yup';
Col,
Text,
} from "@tlon/indigo-react";
import { Formik, Form } from "formik";
import * as Yup from "yup";
import GlobalApi from '~/logic/api/global';
import { uxToHex } from '~/logic/lib/util';
import { S3State, BackgroundConfig } from '@urbit/api';
import { BackgroundPicker, BgType } from './BackgroundPicker';
import useLocalState, { LocalState } from '~/logic/state/local';
import GlobalApi from "~/logic/api/global";
import { uxToHex } from "~/logic/lib/util";
import { S3State, BackgroundConfig } from "~/types";
import { BackgroundPicker, BgType } from "./BackgroundPicker";
import useSettingsState, { SettingsState, selectSettingsState } from "~/logic/state/settings";
import {AsyncButton} from "~/views/components/AsyncButton";
import { BackButton } from "./BackButton";
const formSchema = Yup.object().shape({
bgType: Yup.string()
.oneOf(['none', 'color', 'url'], 'invalid')
.required('Required'),
bgUrl: Yup.string().url(),
bgColor: Yup.string(),
avatars: Yup.boolean(),
nicknames: Yup.boolean()
.oneOf(["none", "color", "url"], "invalid")
.required("Required"),
background: Yup.string(),
});
interface FormSchema {
bgType: BgType;
bgColor: string | undefined;
bgUrl: string | undefined;
avatars: boolean;
nicknames: boolean;
}
interface DisplayFormProps {
@ -37,79 +34,79 @@ interface DisplayFormProps {
s3: S3State;
}
const settingsSel = selectSettingsState(["display"]);
export default function DisplayForm(props: DisplayFormProps) {
const { api, s3 } = props;
const { hideAvatars, hideNicknames, background, set: setLocalState } = useLocalState();
const {
display: {
background,
backgroundType,
}
} = useSettingsState(settingsSel);
let bgColor, bgUrl;
if (background?.type === 'url') {
bgUrl = background.url;
if (backgroundType === "url") {
bgUrl = background;
}
if (background?.type === 'color') {
bgColor = background.color;
if (backgroundType === "color") {
bgColor = background;
}
const bgType = background?.type || 'none';
const bgType = backgroundType || "none";
return (
<Formik
validationSchema={formSchema}
initialValues={
{
bgType,
bgColor: bgColor || '',
bgUrl,
avatars: hideAvatars,
nicknames: hideNicknames
bgType: backgroundType,
bgColor: bgColor || "",
bgUrl
} as FormSchema
}
onSubmit={(values, actions) => {
const bgConfig: BackgroundConfig =
values.bgType === 'color'
? { type: 'color', color: `#${uxToHex(values.bgColor || '0x0')}` }
: values.bgType === 'url'
? { type: 'url', url: values.bgUrl || '' }
: undefined;
onSubmit={async (values, actions) => {
let promises = [] as Promise<any>[];
promises.push(api.settings.putEntry('display', 'backgroundType', values.bgType));
promises.push(
api.settings.putEntry('display', 'background',
values.bgType === "color"
? `#${uxToHex(values.bgColor || "0x0")}`
: values.bgType === "url"
? values.bgUrl || ""
: false
));
await Promise.all(promises);
actions.setStatus({ success: null });
setLocalState((state: LocalState) => {
state.background = bgConfig;
state.hideAvatars = values.avatars;
state.hideNicknames = values.nicknames;
});
actions.setSubmitting(false);
}}
>
{props => (
{(props) => (
<Form>
<Box
display="grid"
gridTemplateColumns="100%"
gridTemplateRows="auto"
gridRowGap={5}
>
<Box color="black" fontSize={1} mb={3} fontWeight={900}>
Display Preferences
</Box>
<Col p="5" pt="4" gapY="5">
<BackButton/>
<Col gapY="2">
<Text color="black" fontSize={2} fontWeight="medium">
Display Preferences
</Text>
<Text gray>
Customize visual interfaces across your Landscape
</Text>
</Col>
<BackgroundPicker
bgType={props.values.bgType}
bgUrl={props.values.bgUrl}
api={api}
s3={s3}
/>
<Checkbox
label="Disable avatars"
id="avatars"
caption="Do not show user-set avatars"
/>
<Checkbox
label="Disable nicknames"
id="nicknames"
caption="Do not show user-set nicknames"
/>
<Button border={1} style={{ cursor: 'pointer' }} borderColor="washedGray" type="submit">
<AsyncButton primary width="fit-content" type="submit">
Save
</Button>
</Box>
</AsyncButton>
</Col>
</Form>
)}
</Formik>

View File

@ -0,0 +1,101 @@
import React, { useCallback } from "react";
import _ from "lodash";
import {
Col,
Text,
ManagedToggleSwitchField as Toggle,
ManagedCheckboxField,
BaseInput,
} from "@tlon/indigo-react";
import { Form, FormikHelpers, useField, useFormikContext } from "formik";
import { FormikOnBlur } from "~/views/components/FormikOnBlur";
import { BackButton } from "./BackButton";
import GlobalApi from "~/logic/api/global";
import {
NotificationGraphConfig,
LeapCategories,
leapCategories,
} from "~/types";
import useSettingsState, { selectSettingsState } from "~/logic/state/settings";
import { ShuffleFields } from "~/views/components/ShuffleFields";
const labels: Record<LeapCategories, string> = {
mychannel: "My Channel",
updates: "Notifications",
profile: "Profile",
messages: "Messages",
logout: "Log Out",
};
interface FormSchema {
categories: { display: boolean; category: LeapCategories }[];
}
function CategoryCheckbox(props: { index: number }) {
const { index } = props;
const { values } = useFormikContext<FormSchema>();
const cats = values.categories;
const catNameId = `categories[${index}].category`;
const [field] = useField(catNameId);
const { category } = cats[index];
const label = labels[category];
return (
<ManagedCheckboxField id={`categories[${index}].display`} label={label} />
);
}
const settingsSel = selectSettingsState(["leap", "set"]);
export function LeapSettings(props: { api: GlobalApi; }) {
const { api } = props;
const { leap, set: setSettingsState } = useSettingsState(settingsSel);
const categories = leap.categories as LeapCategories[];
const missing = _.difference(leapCategories, categories);
console.log(categories);
const initialValues = {
categories: [
...categories.map((cat) => ({
category: cat,
display: true,
})),
...missing.map((cat) => ({ category: cat, display: false })),
],
};
const onSubmit = async (values: FormSchema) => {
const result = values.categories.reduce(
(acc, { display, category }) => (display ? [...acc, category] : acc),
[] as LeapCategories[]
);
await api.settings.putEntry('leap', 'categories', result);
};
return (
<Col p="5" pt="4" gapY="5">
<BackButton/>
<Col gapY="1">
<Text fontSize="2" fontWeight="medium">
Leap
</Text>
<Text gray>
Customize Leap ordering, omit modules or results
</Text>
</Col>
<FormikOnBlur initialValues={initialValues} onSubmit={onSubmit}>
<Form>
<Col gapY="4">
<Text fontWeight="medium">
Customize default Leap sections
</Text>
<ShuffleFields name="categories">
{(index, helpers) => <CategoryCheckbox index={index} />}
</ShuffleFields>
</Col>
</Form>
</FormikOnBlur>
</Col>
);
}

View File

@ -0,0 +1,88 @@
import React, { useCallback } from "react";
import {
Col,
Text,
ManagedToggleSwitchField as Toggle,
} from "@tlon/indigo-react";
import { Form, FormikHelpers } from "formik";
import { FormikOnBlur } from "~/views/components/FormikOnBlur";
import { BackButton } from "./BackButton";
import GlobalApi from "~/logic/api/global";
import {NotificationGraphConfig} from "~/types";
interface FormSchema {
mentions: boolean;
dnd: boolean;
watchOnSelf: boolean;
}
export function NotificationPreferences(props: {
api: GlobalApi;
graphConfig: NotificationGraphConfig;
dnd: boolean;
}) {
const { graphConfig, api, dnd } = props;
const initialValues = {
mentions: graphConfig.mentions,
dnd: dnd,
watchOnSelf: graphConfig.watchOnSelf,
};
const onSubmit = useCallback(async (values: FormSchema, actions: FormikHelpers<FormSchema>) => {
try {
let promises: Promise<any>[] = [];
if (values.mentions !== graphConfig.mentions) {
promises.push(api.hark.setMentions(values.mentions));
}
if (values.watchOnSelf !== graphConfig.watchOnSelf) {
promises.push(api.hark.setWatchOnSelf(values.watchOnSelf));
}
if (values.dnd !== dnd && !_.isUndefined(values.dnd)) {
promises.push(api.hark.setDoNotDisturb(values.dnd))
}
await Promise.all(promises);
actions.setStatus({ success: null });
actions.resetForm({ values: initialValues });
} catch (e) {
console.error(e);
actions.setStatus({ error: e.message });
}
}, [api]);
return (
<Col p="5" pt="4" gapY="5">
<BackButton/>
<Col gapY="1">
<Text fontSize="2" fontWeight="medium">
Notification Preferences
</Text>
<Text gray>
Set notification visibility and default behaviours for groups and
messaging
</Text>
</Col>
<FormikOnBlur initialValues={initialValues} onSubmit={onSubmit}>
<Form>
<Col gapY="4">
<Toggle
label="Do not disturb"
id="dnd"
caption="You won't see the notification badge, but notifications will still appear in your inbox."
/>
<Toggle
label="Watch for replies"
id="watchOnSelf"
caption="Automatically follow a post for notifications when it's yours"
/>
<Toggle
label="Watch for mentions"
id="mentions"
caption="Notify me if someone mentions my @p in a channel I've joined"
/>
</Col>
</Form>
</FormikOnBlur>
</Col>
);
}

View File

@ -5,12 +5,14 @@ import {
ManagedTextInputField as Input,
ManagedForm as Form,
Box,
Text,
Button,
Col
Col,
Anchor
} from '@tlon/indigo-react';
import { BucketList } from './BucketList';
import GlobalApi from '~/logic/api/global';
import GlobalApi from "~/logic/api/global";
import { BucketList } from "./BucketList";
import { S3State } from '~/types/s3-update';
interface FormSchema {
@ -47,7 +49,7 @@ export default function S3Form(props: S3FormProps): ReactElement {
);
return (
<>
<Col>
<Col p="5" pt="4" borderBottom="1" borderBottomColor="washedGray">
<Formik
initialValues={
{
@ -60,30 +62,50 @@ export default function S3Form(props: S3FormProps): ReactElement {
}
onSubmit={onSubmit}
>
<Form
display="grid"
gridTemplateColumns="100%"
gridAutoRows="auto"
gridRowGap={5}
>
<Box color="black" fontSize={1} fontWeight={900}>
S3 Credentials
</Box>
<Input label="Endpoint" id="s3endpoint" />
<Input label="Access Key ID" id="s3accessKeyId" />
<Input
type="password"
label="Secret Access Key"
id="s3secretAccessKey"
/>
<Button style={{ cursor: 'pointer' }} type="submit">Submit</Button>
<Form>
<Col maxWidth="600px" gapY="5">
<BackButton/>
<Col gapY="1">
<Text color="black" fontSize={2} fontWeight="medium">
S3 Storage Setup
</Text>
<Text gray>
Store credentials for your S3 object storage buckets on your
Urbit ship, and upload media freely to various modules.
<Anchor
target="_blank"
style={{ textDecoration: 'none' }}
borderBottom="1"
ml="1"
href="https://urbit.org/using/operations/using-your-ship/#bucket-setup">
Learn more
</Anchor>
</Text>
</Col>
<Input label="Endpoint" id="s3endpoint" />
<Input label="Access Key ID" id="s3accessKeyId" />
<Input
type="password"
label="Secret Access Key"
id="s3secretAccessKey"
/>
<Button style={{ cursor: "pointer" }} type="submit">
Submit
</Button>
</Col>
</Form>
</Formik>
</Col>
<Col>
<Box color="black" mb={4} fontSize={1} fontWeight={700}>
S3 Buckets
</Box>
<Col maxWidth="600px" p="5" gapY="4">
<Col gapY="1">
<Text color="black" mb={4} fontSize={2} fontWeight="medium">
S3 Buckets
</Text>
<Text gray>
Your 'active' bucket will be the one used when Landscape uploads a
file
</Text>
</Col>
<BucketList
buckets={s3.configuration.buckets}
selected={s3.configuration.currentBucket}

View File

@ -1,41 +1,60 @@
import React from 'react';
import { Box, Button } from '@tlon/indigo-react';
import React, { useState } from "react";
import {
Box,
Text,
Button,
Col,
StatelessCheckboxField,
} from "@tlon/indigo-react";
import GlobalApi from '../../../../api/global';
import GlobalApi from "~/logic/api/global";
import { BackButton } from "./BackButton";
interface SecuritySettingsProps {
api: GlobalApi;
}
export default function SecuritySettings({ api }: SecuritySettingsProps) {
const [allSessions, setAllSessions] = useState(false);
return (
<Box display="grid" gridTemplateRows="auto" gridTemplateColumns="1fr" gridRowGap={2}>
<Box color="black" fontSize={1} mb={4} fontWeight={900}>
Security
</Box>
<Box color="black" fontSize={0} fontWeight={700}>
Log out of this session
</Box>
<Box fontSize={0} mt={2} color="gray">
You will be logged out of your Urbit on this browser.
<Col gapY="5" p="5" pt="4">
<BackButton/>
<Col gapY="1">
<Text fontSize={2} fontWeight="medium">
Security Preferences
</Text>
<Text gray>
Manage sessions, login credentials and Landscape access
</Text>
</Col>
<Col gapY="1">
<Text color="black">
Log out of this session
</Text>
<Text mb="3" gray>
{allSessions
? "You will be logged out of all browsers that have currently logged into your Urbit."
: "You will be logged out of your Urbit on this browser."}
</Text>
<StatelessCheckboxField
mb="3"
selected={allSessions}
onChange={() => setAllSessions((s) => !s)}
>
<Text>Log out of all sessions</Text>
</StatelessCheckboxField>
<form method="post" action="/~/logout">
<Button mt='4' border={1} style={{ cursor: 'pointer' }}>
{allSessions && <input type="hidden" name="all" />}
<Button
primary
destructive
border={1}
style={{ cursor: "pointer" }}
>
Logout
</Button>
</form>
</Box>
<Box color="black" fontSize={0} mt={4} fontWeight={700}>
Log out of all sessions
</Box>
<Box fontSize={0} mt={2} color="gray">
You will be logged out of all browsers that have currently logged into your Urbit.
<form method="post" action="/~/logout">
<input type="hidden" name="all" />
<Button destructive mt={4} border={1} style={{ cursor: 'pointer' }}>
Logout
</Button>
</form>
</Box>
</Box>
</Col>
</Col>
);
}

View File

@ -1,37 +1,97 @@
import React from 'react';
import React from "react";
import { Box } from '@tlon/indigo-react';
import { Row, Icon, Box, Col, Text } from "@tlon/indigo-react";
import GlobalApi from '~/logic/api/global';
import { StoreState } from '~/logic/store/type';
import DisplayForm from './lib/DisplayForm';
import S3Form from './lib/S3Form';
import SecuritySettings from './lib/Security';
import RemoteContentForm from './lib/RemoteContent';
import GlobalApi from "~/logic/api/global";
import { StoreState } from "~/logic/store/type";
import DisplayForm from "./lib/DisplayForm";
import S3Form from "./lib/S3Form";
import SecuritySettings from "./lib/Security";
import RemoteContentForm from "./lib/RemoteContent";
import { NotificationPreferences } from "./lib/NotificationPref";
import { CalmPrefs } from "./lib/CalmPref";
import { Link } from "react-router-dom";
type ProfileProps = StoreState & { api: GlobalApi; ship: string };
export default function Settings({
api,
s3
}: ProfileProps) {
export function SettingsItem(props: {
title: string;
description: string;
to: string;
}) {
const { to, title, description } = props;
return (
<Box
backgroundColor="white"
display="grid"
gridTemplateRows="auto"
gridTemplateColumns="1fr"
gridRowGap={7}
p={4}
maxWidth="500px"
>
<DisplayForm
api={api}
s3={s3}
/>
<RemoteContentForm api={api} />
<S3Form api={api} s3={s3} />
<SecuritySettings api={api} />
</Box>
<Link to={`/~settings/${to}`}>
<Row alignItems="center" gapX="3">
<Box
borderRadius="2"
backgroundColor="blue"
width="64px"
height="64px"
/>
<Col gapY="2">
<Text>{title}</Text>
<Text gray>{description}</Text>
</Col>
</Row>
</Link>
);
}
export default function Settings(props: {}) {
return (
<Col gapY="5" p="5">
<Col gapY="1">
<Text fontSize="2">System Preferences</Text>
<Text gray>Configure and customize Landscape</Text>
</Col>
<Box
display="grid"
width="100%"
height="100%"
gridTemplateColumns={["100%", "1fr 1fr"]}
gridGap="3"
>
<SettingsItem
to="notifications"
title="Notifications"
description="Set notification visibility and default behaviours for groups and messaging"
/>
<SettingsItem
to="display"
title="Display"
description="Customize visual interfaces across your Landscape"
/>
<SettingsItem
to="calm"
title="CalmEngine"
description="Modulate vearious elements across Landscape to maximize calmness"
/>
<SettingsItem
to="s3"
title="Remote Storage"
description="Configure S3-compatible storage solutions"
/>
<SettingsItem
to="security"
title="Security"
description="Manage sessions, login credentials, and Landscape access"
/>
{/*
<SettingsItem
to="keyboard"
title="Keyboard"
description="Shortcuts, Keyboard Settings, Meta Key Assignments, etc."
/>
<SettingsItem
to="hosting"
title="Hosting Services"
description="Hosting-specific service configuration"
/>*/}
<SettingsItem
to="leap"
title="Leap"
description="Customize Leap ordering, omit modules or results"
/>
</Box>
</Col>
);
}

View File

@ -1,47 +1,139 @@
import React, { ReactElement } from 'react';
import { Route } from 'react-router-dom';
import Helmet from 'react-helmet';
import React, { ReactNode } from "react";
import { useLocation } from "react-router-dom";
import Helmet from "react-helmet";
import { Box } from '@tlon/indigo-react';
import { Text, Box, Col, Row } from '@tlon/indigo-react';
import Settings from './components/settings';
import useLocalState from '~/logic/state/local';
import { NotificationPreferences } from "./components/lib/NotificationPref";
import DisplayForm from "./components/lib/DisplayForm";
import S3Form from "./components/lib/S3Form";
import { CalmPrefs } from "./components/lib/CalmPref";
import SecuritySettings from "./components/lib/Security";
import { LeapSettings } from "./components/lib/LeapSettings";
import { useHashLink } from "~/logic/lib/useHashLink";
import { SidebarItem as BaseSidebarItem } from "~/views/landscape/components/SidebarItem";
import { PropFunc } from "~/types";
export const Skeleton = (props: { children: ReactNode }) => (
<Box height="100%" width="100%" px={[0, 3]} pb={[0, 3]} borderRadius={1}>
<Box
height="100%"
width="100%"
borderRadius={1}
bg="white"
border={1}
borderColor="washedGray"
>
{props.children}
</Box>
</Box>
);
type ProvSideProps = "to" | "selected";
type BaseProps = PropFunc<typeof BaseSidebarItem>;
function SidebarItem(props: { hash: string } & Omit<BaseProps, ProvSideProps>) {
const { hash, icon, text, ...rest } = props;
const to = `/~settings#${hash}`;
const location = useLocation();
const selected = location.hash.slice(1) === hash;
return (
<BaseSidebarItem
{...rest}
icon={icon}
text={text}
to={to}
selected={selected}
/>
);
}
function SettingsItem(props: { children: ReactNode }) {
const { children } = props;
return (
<Box borderBottom="1" borderBottomColor="washedGray">
{children}
</Box>
);
}
export default function SettingsScreen(props: any) {
const location = useLocation();
const hash = location.hash.slice(1)
export default function SettingsScreen(props: any): ReactElement {
return (
<>
<Helmet defer={false}>
<title>Landscape - Settings</title>
</Helmet>
<Route
path={['/~settings']}
render={() => {
return (
<Box height="100%"
width="100%"
px={[0, 3]}
pb={[0, 3]}
borderRadius={1}
<Skeleton>
<Row height="100%" overflow="hidden">
<Col
height="100%"
borderRight="1"
borderRightColor="washedGray"
display={hash === "" ? "flex" : ["none", "flex"]}
minWidth="250px"
width="100%"
maxWidth={["100vw", "350px"]}
>
<Text
display="block"
my="4"
mx="3"
fontSize="2"
fontWeight="medium"
>
<Box
height="100%"
width="100%"
display="grid"
gridTemplateColumns={['100%', '400px 1fr']}
gridTemplateRows={['48px 1fr', '1fr']}
borderRadius={1}
bg="white"
border={1}
borderColor="washedGray"
overflowY="auto"
flexGrow
>
<Settings {...props} />
</Box>
</Box>
);
}}
/>
System Preferences
</Text>
<Col gapY="1">
<SidebarItem
icon="Inbox"
text="Notifications"
hash="notifications"
/>
<SidebarItem icon="Image" text="Display" hash="display" />
<SidebarItem icon="Upload" text="Remote Storage" hash="s3" />
<SidebarItem icon="LeapArrow" text="Leap" hash="leap" />
<SidebarItem icon="Node" text="CalmEngine" hash="calm" />
<SidebarItem
icon="Locked"
text="Devices + Security"
hash="security"
/>
</Col>
</Col>
<Col flexGrow={1} overflowY="auto">
<SettingsItem>
{hash === "notifications" && (
<NotificationPreferences
{...props}
graphConfig={props.notificationsGraphConfig}
/>
)}
{hash === "display" && (
<DisplayForm s3={props.s3} api={props.api} />
)}
{hash === "s3" && (
<S3Form s3={props.s3} api={props.api} />
)}
{hash === "leap" && (
<LeapSettings api={props.api} />
)}
{hash === "calm" && (
<CalmPrefs api={props.api} />
)}
{hash === "security" && (
<SecuritySettings api={props.api} />
)}
</SettingsItem>
</Col>
</Row>
</Skeleton>
</>
);
}

View File

@ -5,7 +5,7 @@ import { Box } from '@tlon/indigo-react';
export function Body(
props: { children: ReactNode } & Parameters<typeof Box>[0]
) {
const { children, ...boxProps } = props;
const { children, border, ...boxProps } = props;
return (
<Box fontSize={0} px={[0, 3]} pb={[0, 3]} height="100%" width="100%">
<Box
@ -13,11 +13,11 @@ export function Body(
height="100%"
width="100%"
borderRadius={2}
border={[0, 1]}
border={border ? border : [0, 1]}
borderColor={['washedGray', 'washedGray']}
{...boxProps}
>
{props.children}
{children}
</Box>
</Box>
);

View File

@ -14,12 +14,13 @@ import { hexToUx } from '~/logic/lib/util';
type ColorInputProps = Parameters<typeof Col>[0] & {
id: string;
label: string;
label?: string;
placeholder?: string;
disabled?: boolean;
};
export function ColorInput(props: ColorInputProps): ReactElement {
const { id, label, caption, disabled, ...rest } = props;
export function ColorInput(props: ColorInputProps) {
const { id, placeholder, label, caption, disabled, ...rest } = props;
const [{ value, onBlur }, meta, { setValue }] = useField(id);
const hex = value.replace('#', '').replace('0x','').replace('.', '');
@ -54,6 +55,7 @@ export function ColorInput(props: ColorInputProps): ReactElement {
value={hex}
disabled={disabled || false}
borderRight={0}
placeholder={placeholder}
/>
<Box
borderBottomRightRadius={1}

View File

@ -41,6 +41,7 @@ const Candidate = ({ title, selected, onClick }): ReactElement => (
<CandidateBox
onClick={onClick}
selected={selected}
cursor="pointer"
borderColor="washedGray"
color="black"
fontSize={0}

View File

@ -8,10 +8,10 @@ interface LoadingProps {
}
export function Loading({ text }: LoadingProps) {
return (
<Body>
<Body border="0">
<Center height="100%">
<LoadingSpinner />
{Boolean(text) && <Text>{text}</Text>}
{Boolean(text) && <Text ml={4}>{text}</Text>}
</Center>
</Body>
);

View File

@ -78,6 +78,7 @@ const Candidate = ({ title, detail, selected, onClick }): ReactElement => (
bg="white"
color="black"
fontSize={0}
cursor="pointer"
p={1}
width="100%"
>

View File

@ -0,0 +1,56 @@
import React, { ReactNode, useMemo, useCallback } from "react";
import {
FieldArray,
FieldArrayRenderProps,
Field,
useFormikContext,
} from "formik";
import { Icon, Col, Row, Box } from "@tlon/indigo-react";
interface ShuffleFieldsProps<N extends string> {
name: N;
children: (index: number, props: FieldArrayRenderProps) => ReactNode;
}
type Value<I extends string, T> = {
[k in I]: T[];
};
export function ShuffleFields<N extends string, T, F extends Value<N, T>>(
props: ShuffleFieldsProps<N>
) {
const { name, children } = props;
const { values } = useFormikContext<F>();
const fields: T[] = useMemo(() => values[name], [values, name]);
return (
<FieldArray
name={name}
render={(arrayHelpers) => {
const goUp = (i: number) => () => {
if(i > 0) {
arrayHelpers.swap(i - 1, i);
}
};
const goDown = (i: number) => () => {
if(i < fields.length - 1) {
arrayHelpers.swap(i + 1, i);
}
};
return (
<Box gridColumnGap="2" gridRowGap="3" display="grid" gridAutoRows="auto" gridTemplateColumns="32px 32px 1fr">
{fields.map((field, i) => (
<React.Fragment key={i}>
<Icon width="3" height="3" icon="ChevronNorth" onClick={goUp(i)} />
<Icon width="3" height="3" icon="ChevronSouth" onClick={goDown(i)} />
{children(i, arrayHelpers)}
</React.Fragment>
))}
</Box>
);
}}
/>
);
}

View File

@ -21,17 +21,18 @@ import { uxToHex } from "~/logic/lib/util";
import { SetStatusBarModal } from './SetStatusBarModal';
import { useTutorialModal } from './useTutorialModal';
import useLocalState from '~/logic/state/local';
import useLocalState, { selectLocalState } from '~/logic/state/local';
import useSettingsState, { selectCalmState } from '~/logic/state/settings';
const localSel = selectLocalState(['toggleOmnibox']);
const StatusBar = (props) => {
const { ourContact, api, ship } = props;
const invites = [].concat(...Object.values(props.invites).map(obj => Object.values(obj)));
const metaKey = (window.navigator.platform.includes('Mac')) ? '⌘' : 'Ctrl+';
const { toggleOmnibox, hideAvatars } =
useLocalState(({ toggleOmnibox, hideAvatars }) =>
({ toggleOmnibox, hideAvatars })
);
const { toggleOmnibox } = useLocalState(localSel);
const { hideAvatars } = useSettingsState(selectCalmState);
const color = !!ourContact ? `#${uxToHex(props.ourContact.color)}` : '#000';
const xPadding = (!hideAvatars && ourContact?.avatar) ? '0' : '2';

View File

@ -9,12 +9,13 @@ import { Associations, Contacts, Groups, Invites } from '@urbit/api';
import makeIndex from '~/logic/lib/omnibox';
import OmniboxInput from './OmniboxInput';
import OmniboxResult from './OmniboxResult';
import { withLocalState } from '~/logic/state/local';
import { deSig } from '~/logic/lib/util';
import { withLocalState } from '~/logic/state/local';
import defaultApps from '~/logic/lib/default-apps';
import { useOutsideClick } from '~/logic/lib/useOutsideClick';
import { Portal } from '../Portal';
import {useOutsideClick} from '~/logic/lib/useOutsideClick';
import {Portal} from '../Portal';
import useSettingsState, {SettingsState} from '~/logic/state/settings';
import { Tile } from '~/types';
interface OmniboxProps {
@ -31,11 +32,13 @@ interface OmniboxProps {
}
const SEARCHED_CATEGORIES = ['ships', 'other', 'commands', 'groups', 'subscriptions', 'apps'];
const settingsSel = (s: SettingsState) => s.leap;
export function Omnibox(props: OmniboxProps) {
const location = useLocation();
const history = useHistory();
const omniboxRef = useRef<HTMLDivElement | null>(null);
const leapConfig = useSettingsState(settingsSel);
const omniboxRef = useRef<HTMLDivElement | null>(null)
const inputRef = useRef<HTMLInputElement | null>(null);
const [query, setQuery] = useState('');
@ -48,18 +51,30 @@ export function Omnibox(props: OmniboxProps) {
: props.contacts;
}, [props.contacts, query]);
const index = useMemo(() => {
const selectedGroup = location.pathname.startsWith('/~landscape/ship/')
const selectedGroup = useMemo(
() => location.pathname.startsWith('/~landscape/ship/')
? '/' + location.pathname.split('/').slice(2,5).join('/')
: null;
: null,
[location.pathname]
);
const index = useMemo(() => {
return makeIndex(
contacts,
props.associations,
props.tiles,
selectedGroup,
props.groups
props.groups,
leapConfig,
);
}, [location.pathname, contacts, props.associations, props.groups, props.tiles]);
}, [
selectedGroup,
leapConfig,
contacts,
props.associations,
props.groups,
props.tiles
]);
const onOutsideClick = useCallback(() => {
props.show && props.toggle();

View File

@ -2,7 +2,7 @@ import React, { Component } from 'react';
import { Box, Row, Icon, Text } from '@tlon/indigo-react';
import defaultApps from '~/logic/lib/default-apps';
import Sigil from '~/logic/lib/sigil';
import { uxToHex } from '~/logic/lib/util';
import { uxToHex, cite } from '~/logic/lib/util';
export class OmniboxResult extends Component {
constructor(props) {
@ -90,25 +90,29 @@ export class OmniboxResult extends Component {
}
onClick={navigate}
width="100%"
justifyContent="space-between"
ref={this.result}
>
<Box display="flex" verticalAlign="middle" maxWidth="60%" flexShrink={0}>
{graphic}
<Text
display="inline-block"
verticalAlign="middle"
mono={(icon == 'profile' && text.startsWith('~'))}
color={this.state.hovered || selected === link ? 'white' : 'black'}
maxWidth="60%"
style={{ flexShrink: 0 }}
mr='1'
>
{text}
{text.startsWith("~") ? cite(text) : text}
</Text>
</Box>
<Text pr='2'
display="inline-block"
verticalAlign="middle"
color={this.state.hovered || selected === link ? 'white' : 'black'}
width='100%'
minWidth={0}
textOverflow="ellipsis"
whiteSpace="pre"
overflow="hidden"
maxWidth="40%"
textAlign='right'
>
{subtext}

View File

@ -151,7 +151,7 @@ export function GraphPermissions(props: GraphPermissionsProps) {
}
};
const schema = formSchema(Array.from(group.members));
const schema = formSchema(Array.from(group?.members ?? []));
return (
<Formik

View File

@ -22,6 +22,7 @@ import { isChannelAdmin, isHost } from '~/logic/lib/group';
interface ChannelPopoverRoutesProps {
baseUrl: string;
rootUrl: string;
association: Association;
group: Group;
groups: Groups;
@ -51,8 +52,8 @@ export function ChannelPopoverRoutes(props: ChannelPopoverRoutesProps) {
};
const handleArchive = async () => {
const [,,,name] = association.resource.split('/');
await api.graph.deleteGraph(name);
history.push(props.baseUrl);
api.graph.deleteGraph(name);
return history.push(props.rootUrl);
};
const canAdmin = isChannelAdmin(group, association.resource);

View File

@ -1,4 +1,4 @@
import React, { Component } from 'react';
import React, { Component, useEffect } from 'react';
import { Box } from '@tlon/indigo-react';
import { Route, Switch } from 'react-router-dom';
import styled from 'styled-components';
@ -12,6 +12,8 @@ import ErrorComponent from '~/views/components/Error';
import Notifications from '~/views/apps/notifications/notifications';
import GraphApp from '../../apps/graph/app';
import { useMigrateSettings } from '~/logic/lib/migrateSettings';
export const Container = styled(Box)`
flex-grow: 1;
@ -22,6 +24,14 @@ export const Container = styled(Box)`
export const Content = (props) => {
const doMigrate = useMigrateSettings();
useEffect(() => {
setTimeout(() => {
doMigrate();
}, 10000);
}, []);
return (
<Container>
<Switch>

View File

@ -206,7 +206,7 @@ export function GroupsPane(props: GroupsPaneProps) {
resource={groupAssociation.group}
/>;
} else {
summary = (<Box p="4"><Text fontSize="0" color='gray'>
summary = (<Box p="4"><Text color='gray'>
Create or select a channel to get started
</Text></Box>);
}

View File

@ -115,10 +115,14 @@ export function NewChannel(props: NewChannelProps & RouteComponentProps): ReactE
return (
<Col overflowY="auto" p={3} backgroundColor="white">
<Box pb='3' display={['block', 'none']} onClick={() => history.push(props.baseUrl)}>
<Text fontSize='0' bold>{'<- Back'}</Text>
<Box
pb='3'
display={workspace?.type === 'messages' ? 'none' : ['block', 'none']}
onClick={() => history.push(props.baseUrl)}
>
<Text>{'<- Back'}</Text>
</Box>
<Box color="black">
<Box>
<Text fontSize={2} bold>{workspace?.type === 'messages' ? 'Direct Message' : 'New Channel'}</Text>
</Box>
<Formik

View File

@ -29,7 +29,7 @@ import { roleForShip, resourceFromPath } from '~/logic/lib/group';
import { Dropdown } from '~/views/components/Dropdown';
import GlobalApi from '~/logic/api/global';
import { StatelessAsyncAction } from '~/views/components/StatelessAsyncAction';
import useLocalState from '~/logic/state/local';
import useSettingsState, { selectCalmState } from '~/logic/state/settings';
const TruncText = styled(Text)`
white-space: nowrap;
@ -79,13 +79,14 @@ function getParticipants(cs: Contacts, group: Group) {
const emptyContact = (patp: string, pending: boolean): Participant => ({
nickname: '',
email: '',
phone: '',
bio: '',
status: '',
color: '',
avatar: null,
notes: '',
website: '',
cover: null,
groups: [],
patp,
'last-updated': 0,
pending
});
@ -256,9 +257,7 @@ function Participant(props: {
}) {
const { contact, association, group, api } = props;
const { title } = association.metadata;
const { hideAvatars, hideNicknames } = useLocalState(
({ hideAvatars, hideNicknames }) => ({ hideAvatars, hideNicknames })
);
const { hideAvatars, hideNicknames } = useSettingsState(selectCalmState);
const color = uxToHex(contact.color);
const isInvite = 'invite' in group.policy;

View File

@ -1,5 +1,5 @@
import React, { ReactElement, ReactNode } from 'react';
import { Icon, Box, Col, Text } from '@tlon/indigo-react';
import { Icon, Box, Col, Row, Text } from '@tlon/indigo-react';
import styled from 'styled-components';
import { Link } from 'react-router-dom';
import urbitOb from 'urbit-ob';
@ -12,7 +12,7 @@ import GlobalApi from '~/logic/api/global';
import { isWriter } from '~/logic/lib/group';
import { getItemTitle } from '~/logic/lib/util';
const TruncatedBox = styled(Box)`
const TruncatedText = styled(RichText)`
white-space: pre;
text-overflow: ellipsis;
overflow: hidden;
@ -46,11 +46,13 @@ export function ResourceSkeleton(props: ResourceSkeletonProps): ReactElement {
? getItemTitle(association)
: association?.metadata?.title;
let recipient = false;
let recipient = "";
if (urbitOb.isValidPatp(title)) {
recipient = title;
title = (props.contacts?.[title]?.nickname) ? props.contacts[title].nickname : title;
} else {
recipient = Array.from(group.members).map(e => `~${e}`).join(", ")
}
const [, , ship, resource] = rid.split('/');
@ -88,7 +90,7 @@ export function ResourceSkeleton(props: ResourceSkeletonProps): ReactElement {
>
<Link to={`/~landscape${workspace}`}> {'<- Back'}</Link>
</Box>
<Box px={1} mr={2} minWidth={0} display="flex">
<Box px={1} mr={2} minWidth={0} display="flex" flexShrink={[1, 0]}>
<Text
mono={urbitOb.isValidPatp(title)}
fontSize='2'
@ -99,29 +101,31 @@ export function ResourceSkeleton(props: ResourceSkeletonProps): ReactElement {
overflow="hidden"
whiteSpace="pre"
minWidth={0}
flexShrink={1}
>
{title}
</Text>
</Box>
<TruncatedBox
display={['none', 'block']}
<Row
display={['none', 'flex']}
verticalAlign="middle"
maxWidth='60%'
flexShrink={1}
flexShrink={2}
minWidth={0}
title={association?.metadata?.description}
color="gray"
>
<RichText
<TruncatedText
display={(workspace === '/messages' && (urbitOb.isValidPatp(title))) ? 'none' : 'inline-block'}
mono={(workspace === '/messages' && !(urbitOb.isValidPatp(title)))}
color="gray"
minWidth={0}
width="100%"
mb="0"
disableRemoteContent
>
{(workspace === '/messages') ? recipient : association?.metadata?.description}
</RichText>
</TruncatedBox>
<Box flexGrow={1} />
</TruncatedText>
</Row>
<Box flexGrow={1} flexShrink={0} />
{canWrite && (
<Link to={resourcePath('/new')} style={{ flexShrink: '0' }}>
<Text bold pr='3' color='blue'>+ New Post</Text>

View File

@ -8,10 +8,11 @@ import { HoverBoxLink } from '~/views/components/HoverBox';
import { Sigil } from '~/logic/lib/sigil';
import { getModuleIcon, getItemTitle, uxToHex } from '~/logic/lib/util';
import { useTutorialModal } from '~/views/components/useTutorialModal';
import useLocalState from '~/logic/state/local';
import { TUTORIAL_HOST, TUTORIAL_GROUP } from '~/logic/lib/tutorialModal';
import { SidebarAppConfigs, SidebarItemStatus } from './types';
import { Workspace } from '~/types/workspace';
import useSettingsState, { selectCalmState } from '~/logic/state/settings';
function SidebarItemIndicator(props: { status?: SidebarItemStatus }) {
switch (props.status) {
@ -56,9 +57,8 @@ export function SidebarItem(props: {
return null;
}
const DM = (isUnmanaged && props.workspace?.type === 'messages');
const { hideAvatars, hideNicknames } = useLocalState(({ hideAvatars, hideNicknames }) => ({
hideAvatars, hideNicknames
}));
const { hideAvatars, hideNicknames } = useSettingsState(selectCalmState);
const itemStatus = app.getStatus(path);
const hasUnread = itemStatus === 'unread' || itemStatus === 'mention';

View File

@ -34,7 +34,7 @@ export const SidebarItem = ({
justifyContent="space-between"
{...rest}
>
<Row>
<Row alignItems="center">
<Icon color={color} icon={icon as any} mr="2" />
<Text color={color}>{text}</Text>
</Row>

View File

@ -1,50 +1,2 @@
import { Enc, Path, Patp, Poke } from "..";
import {
Contact,
ContactUpdateAdd,
ContactUpdateEdit,
ContactUpdateRemove,
ContactEditField,
ContactShare,
ContactUpdate,
} from "./index.d";
export const storeAction = <T extends ContactUpdate>(data: T): Poke<T> => ({
app: "contact-store",
mark: "contact-action",
json: data,
});
export const add = (ship: Patp, contact: Contact): Poke<ContactUpdateAdd> => {
contact["last-updated"] = Date.now();
return storeAction({
add: { ship, contact },
});
};
export const remove = (ship: Patp): Poke<ContactUpdateRemove> =>
storeAction({
remove: { ship },
});
export const share = (recipient: Patp): Poke<ContactShare> => ({
app: "contact-push-hook",
mark: "contact-action",
json: { share: recipient },
});
export const edit = (
path: Path,
ship: Patp,
editField: ContactEditField
): Poke<ContactUpdateEdit> =>
storeAction({
edit: {
path,
ship,
"edit-field": editField,
timestamp: Date.now(),
},
});
export * from './types';
export * from './lib';

View File

@ -0,0 +1,94 @@
import { Path, Patp, Poke, resourceAsPath } from "../lib";
import {
Contact,
ContactUpdateAdd,
ContactUpdateEdit,
ContactUpdateRemove,
ContactEditField,
ContactShare,
ContactUpdate,
ContactUpdateAllowShips,
ContactUpdateAllowGroup,
ContactUpdateSetPublic,
} from "./types";
const storeAction = <T extends ContactUpdate>(data: T): Poke<T> => ({
app: "contact-store",
mark: "contact-action",
json: data,
});
export { storeAction as contactStoreAction };
export const addContact = (ship: Patp, contact: Contact): Poke<ContactUpdateAdd> => {
contact["last-updated"] = Date.now();
return storeAction({
add: { ship, contact },
});
};
export const removeContact = (ship: Patp): Poke<ContactUpdateRemove> =>
storeAction({
remove: { ship },
});
export const share = (recipient: Patp): Poke<ContactShare> => ({
app: "contact-push-hook",
mark: "contact-action",
json: { share: recipient },
});
export const editContact = (
ship: Patp,
editField: ContactEditField
): Poke<ContactUpdateEdit> =>
storeAction({
edit: {
ship,
"edit-field": editField,
timestamp: Date.now(),
},
});
export const allowShips = (
ships: Patp[]
): Poke<ContactUpdateAllowShips> => storeAction({
allow: {
ships
}
});
export const allowGroup = (
ship: string,
name: string
): Poke<ContactUpdateAllowGroup> => storeAction({
allow: {
group: resourceAsPath({ ship, name })
}
});
export const setPublic = (
setPublic: any
): Poke<ContactUpdateSetPublic> => {
return storeAction({
'set-public': setPublic
});
}
export const retrieve = (
ship: string
) => {
const resource = { ship, name: '' };
return {
app: 'contact-pull-hook',
mark: 'pull-hook-action',
json: {
add: {
resource,
ship
}
}
};
}

View File

@ -1,47 +1,49 @@
import { Path, Patp } from "..";
import {Resource} from "../groups/update.d";
import { Resource } from "../groups";
export type ContactUpdate =
| ContactUpdateAdd
| ContactUpdateRemove
| ContactUpdateEdit
| ContactUpdateInitial
| ContactUpdateAllowGroup
| ContactUpdateAllowShips
| ContactUpdateSetPublic;
interface ContactUpdateAdd {
export interface ContactUpdateAdd {
add: {
ship: Patp;
contact: Contact;
};
}
interface ContactUpdateRemove {
export interface ContactUpdateRemove {
remove: {
ship: Patp;
};
}
interface ContactUpdateEdit {
export interface ContactUpdateEdit {
edit: {
path: Path;
ship: Patp;
"edit-field": ContactEditField;
timestamp: number;
};
}
interface ContactUpdateAllowShips {
export interface ContactUpdateAllowShips {
allow: {
ships: Patp[];
}
}
interface ContactUpdateAllowGroup {
export interface ContactUpdateAllowGroup {
allow: {
group: Path;
}
}
interface ContactUpdateSetPublic {
export interface ContactUpdateSetPublic {
'set-public': boolean;
}
@ -49,7 +51,7 @@ export interface ContactShare {
share: Patp;
}
interface ContactUpdateInitial {
export interface ContactUpdateInitial {
initial: Rolodex;
}
@ -57,6 +59,8 @@ export type Rolodex = {
[p in Patp]: Contact;
};
export type Contacts = Rolodex;
export interface Contact {
nickname: string;
bio: string;

View File

@ -1,370 +1,2 @@
import _ from 'lodash';
import { PatpNoSig, Patp, Poke, Thread, Path, Enc } from '..';
import { Content, GraphNode, Post, GraphNodePoke, GraphChildrenPoke } from './index.d';
import { deSig, unixToDa } from '../lib/util';
import { makeResource, resourceFromPath } from '../groups/index';
import { GroupPolicy } from '../groups/update.d';
export const createBlankNodeWithChildPost = (
ship: PatpNoSig,
parentIndex: string = '',
childIndex: string = '',
contents: Content[]
): GraphNodePoke => {
const date = unixToDa(Date.now()).toString();
const nodeIndex = parentIndex + '/' + date;
const childGraph: GraphChildrenPoke = {};
childGraph[childIndex] = {
post: {
author: `~${ship}`,
index: nodeIndex + '/' + childIndex,
'time-sent': Date.now(),
contents,
hash: null,
signatures: []
},
children: null
};
return {
post: {
author: `~${ship}`,
index: nodeIndex,
'time-sent': Date.now(),
contents: [],
hash: null,
signatures: []
},
children: childGraph
};
};
function markPending(nodes: any) {
_.forEach(nodes, node => {
node.post.author = deSig(node.post.author);
node.post.pending = true;
markPending(node.children || {});
});
}
export const createPost = (
ship: PatpNoSig,
contents: Content[],
parentIndex: string = '',
childIndex:string = 'DATE_PLACEHOLDER'
): Post => {
if (childIndex === 'DATE_PLACEHOLDER') {
childIndex = unixToDa(Date.now()).toString();
}
return {
author: `~${ship}`,
index: parentIndex + '/' + childIndex,
'time-sent': Date.now(),
contents,
hash: null,
signatures: []
};
};
function moduleToMark(mod: string): string | undefined {
if(mod === 'link') {
return 'graph-validator-link';
}
if(mod === 'publish') {
return 'graph-validator-publish';
}
if(mod === 'chat') {
return 'graph-validator-chat';
}
return undefined;
}
const storeAction = <T>(data: T): Poke<T> => ({
app: 'graph-store',
mark: 'graph-update',
json: data
});
export { storeAction as graphStoreAction };
const viewAction = <T>(threadName: string, action: T): Thread<T> => ({
inputMark: 'graph-view-action',
outputMark: 'json',
threadName,
body: action
});
export { viewAction as graphViewAction };
const hookAction = <T>(data: T): Poke<T> => ({
app: 'graph-push-hook',
mark: 'graph-update',
json: data
});
export { hookAction as graphHookAction };
export const createManagedGraph = (
ship: PatpNoSig,
name: string,
title: string,
description: string,
group: Path,
mod: string
): Thread<any> => {
const associated = { group: resourceFromPath(group) };
const resource = makeResource(`~${ship}`, name);
return viewAction('graph-create', {
create: {
resource,
title,
description,
associated,
module: mod,
mark: moduleToMark(mod)
}
});
}
export const createUnmanagedGraph = (
ship: PatpNoSig,
name: string,
title: string,
description: string,
policy: Enc<GroupPolicy>,
mod: string
): Thread<any> => {
const resource = makeResource(`~${ship}`, name);
return viewAction('graph-create', {
create: {
resource,
title,
description,
associated: { policy },
module: mod,
mark: moduleToMark(mod)
}
});
}
export const joinGraph = (
ship: Patp,
name: string
): Thread<any> => {
const resource = makeResource(ship, name);
return viewAction('graph-join', {
join: {
resource,
ship,
}
});
}
export const deleteGraph = (
ship: PatpNoSig,
name: string
): Thread<any> => {
const resource = makeResource(`~${ship}`, name);
return viewAction('graph-delete', {
"delete": {
resource
}
});
}
export const leaveGraph = (
ship: Patp,
name: string
): Thread<any> => {
const resource = makeResource(ship, name);
return viewAction('graph-leave', {
"leave": {
resource
}
});
}
export const groupifyGraph = (
ship: Patp,
name: string,
toPath?: string
): Thread<any> => {
const resource = makeResource(ship, name);
const to = toPath && resourceFromPath(toPath);
return viewAction('graph-groupify', {
groupify: {
resource,
to
}
});
}
export const evalCord = (
cord: string
): Thread<any> => {
return ({
inputMark: 'graph-view-action',
outputMark: 'tang',
threadName: 'graph-eval',
body: {
eval: cord
}
});
}
export const addGraph = (
ship: Patp,
name: string,
graph: any,
mark: any
): Poke<any> => {
return storeAction({
'add-graph': {
resource: { ship, name },
graph,
mark
}
});
}
export const addPost = (
ship: Patp,
name: string,
post: Post
) => {
let nodes = {};
nodes[post.index] = {
post,
children: null
};
return addNodes(ship, name, nodes);
}
export const addNode = (
ship: Patp,
name: string,
node: GraphNode
) => {
let nodes = {};
nodes[node.post.index] = node;
return addNodes(ship, name, nodes);
}
export const addNodes = (
ship: Patp,
name: string,
nodes: Object
): Poke<any> => {
const action = {
'add-nodes': {
resource: { ship, name },
nodes
}
};
markPending(action['add-nodes'].nodes);
action['add-nodes'].resource.ship = action['add-nodes'].resource.ship.slice(1);
// this.store.handleEvent({ data: { 'graph-update': action } });// TODO address this.store
return hookAction(action);
}
export const removeNodes = (
ship: Patp,
name: string,
indices: string[]
): Poke<any> => {
return hookAction({
'remove-nodes': {
resource: { ship, name },
indices
}
});
}
// TODO these abominations
// getKeys() {
// return this.scry<any>('graph-store', '/keys')
// .then((keys) => {
// this.store.handleEvent({
// data: keys
// });
// });
// }
// getTags() {
// return this.scry<any>('graph-store', '/tags')
// .then((tags) => {
// this.store.handleEvent({
// data: tags
// });
// });
// }
// getTagQueries() {
// return this.scry<any>('graph-store', '/tag-queries')
// .then((tagQueries) => {
// this.store.handleEvent({
// data: tagQueries
// });
// });
// }
// getGraph(ship: string, resource: string) {
// return this.scry<any>('graph-store', `/graph/${ship}/${resource}`)
// .then((graph) => {
// this.store.handleEvent({
// data: graph
// });
// });
// }
// async getNewest(ship: string, resource: string, count: number, index = '') {
// const data = await this.scry<any>('graph-store', `/newest/${ship}/${resource}/${count}${index}`);
// this.store.handleEvent({ data });
// }
// async getOlderSiblings(ship: string, resource: string, count: number, index = '') {
// const idx = index.split('/').map(decToUd).join('/');
// const data = await this.scry<any>('graph-store',
// `/node-siblings/older/${ship}/${resource}/${count}${idx}`
// );
// this.store.handleEvent({ data });
// }
// async getYoungerSiblings(ship: string, resource: string, count: number, index = '') {
// const idx = index.split('/').map(decToUd).join('/');
// const data = await this.scry<any>('graph-store',
// `/node-siblings/younger/${ship}/${resource}/${count}${idx}`
// );
// this.store.handleEvent({ data });
// }
// getGraphSubset(ship: string, resource: string, start: string, end: string) {
// return this.scry<any>(
// 'graph-store',
// `/graph-subset/${ship}/${resource}/${end}/${start}`
// ).then((subset) => {
// this.store.handleEvent({
// data: subset
// });
// });
// }
// getNode(ship: string, resource: string, index: string) {
// const idx = index.split('/').map(numToUd).join('/');
// return this.scry<any>(
// 'graph-store',
// `/node/${ship}/${resource}${idx}`
// ).then((node) => {
// this.store.handleEvent({
// data: node
// });
// });
// }
export * from './lib';
export * from './types';

271
pkg/npm/api/graph/lib.ts Normal file
View File

@ -0,0 +1,271 @@
import _ from 'lodash';
import { GroupPolicy, makeResource, resourceFromPath } from '../groups';
import { deSig, unixToDa } from '../lib';
import { Enc, Path, Patp, PatpNoSig, Poke, Thread } from '../lib/types';
import { Content, GraphChildrenPoke, GraphNode, GraphNodePoke, Post } from './types';
export const createBlankNodeWithChildPost = (
ship: PatpNoSig,
parentIndex: string = '',
childIndex: string = '',
contents: Content[]
): any => { // TODO should be GraphNode
const date = unixToDa(Date.now()).toString();
const nodeIndex = parentIndex + '/' + date;
const childGraph: GraphChildrenPoke = {};
childGraph[childIndex] = {
post: {
author: `~${ship}`,
index: nodeIndex + '/' + childIndex,
'time-sent': Date.now(),
contents,
hash: null,
signatures: []
},
children: null
};
return {
post: {
author: `~${ship}`,
index: nodeIndex,
'time-sent': Date.now(),
contents: [],
hash: null,
signatures: []
},
children: childGraph
};
};
export const markPending = (nodes: any): void => {
_.forEach(nodes, node => {
node.post.author = deSig(node.post.author);
node.post.pending = true;
markPending(node.children || {});
});
};
export const createPost = (
ship: PatpNoSig,
contents: Content[],
parentIndex: string = '',
childIndex:string = 'DATE_PLACEHOLDER'
): Post => {
if (childIndex === 'DATE_PLACEHOLDER') {
childIndex = unixToDa(Date.now()).toString();
}
return {
author: `~${ship}`,
index: parentIndex + '/' + childIndex,
'time-sent': Date.now(),
contents,
hash: null,
signatures: []
};
};
function moduleToMark(mod: string): string | undefined {
if(mod === 'link') {
return 'graph-validator-link';
}
if(mod === 'publish') {
return 'graph-validator-publish';
}
if(mod === 'chat') {
return 'graph-validator-chat';
}
return undefined;
}
const storeAction = <T>(data: T): Poke<T> => ({
app: 'graph-store',
mark: 'graph-update',
json: data
});
export { storeAction as graphStoreAction };
const viewAction = <T>(threadName: string, action: T): Thread<T> => ({
inputMark: 'graph-view-action',
outputMark: 'json',
threadName,
body: action
});
export { viewAction as graphViewAction };
const hookAction = <T>(data: T): Poke<T> => ({
app: 'graph-push-hook',
mark: 'graph-update',
json: data
});
export { hookAction as graphHookAction };
export const createManagedGraph = (
ship: PatpNoSig,
name: string,
title: string,
description: string,
group: Path,
mod: string
): Thread<any> => {
const associated = { group: resourceFromPath(group) };
const resource = makeResource(`~${ship}`, name);
return viewAction('graph-create', {
create: {
resource,
title,
description,
associated,
module: mod,
mark: moduleToMark(mod)
}
});
}
export const createUnmanagedGraph = (
ship: PatpNoSig,
name: string,
title: string,
description: string,
policy: Enc<GroupPolicy>,
mod: string
): Thread<any> => viewAction('graph-create', {
create: {
resource: makeResource(`~${ship}`, name),
title,
description,
associated: { policy },
module: mod,
mark: moduleToMark(mod)
}
});
export const joinGraph = (
ship: Patp,
name: string
): Thread<any> => viewAction('graph-join', {
join: {
resource: makeResource(ship, name),
ship,
}
});
export const deleteGraph = (
ship: PatpNoSig,
name: string
): Thread<any> => viewAction('graph-delete', {
"delete": {
resource: makeResource(`~${ship}`, name)
}
});
export const leaveGraph = (
ship: Patp,
name: string
): Thread<any> => viewAction('graph-leave', {
"leave": {
resource: makeResource(ship, name)
}
});
export const groupifyGraph = (
ship: Patp,
name: string,
toPath?: string
): Thread<any> => {
const resource = makeResource(ship, name);
const to = toPath && resourceFromPath(toPath);
return viewAction('graph-groupify', {
groupify: {
resource,
to
}
});
}
export const evalCord = (
cord: string
): Thread<any> => {
return ({
inputMark: 'graph-view-action',
outputMark: 'tang',
threadName: 'graph-eval',
body: {
eval: cord
}
});
}
export const addGraph = (
ship: Patp,
name: string,
graph: any,
mark: any
): Poke<any> => {
return storeAction({
'add-graph': {
resource: { ship, name },
graph,
mark
}
});
}
export const addNodes = (
ship: Patp,
name: string,
nodes: Object
): Poke<any> => {
const action = {
'add-nodes': {
resource: { ship, name },
nodes
}
};
return hookAction(action);
};
export const addPost = (
ship: Patp,
name: string,
post: Post
) => {
let nodes: Record<string, GraphNode> = {};
nodes[post.index] = {
post,
children: null
};
return addNodes(ship, name, nodes);
}
export const addNode = (
ship: Patp,
name: string,
node: GraphNode
): Poke<any> => {
let nodes: Record<string, GraphNode> = {};
nodes[node.post.index] = node;
return addNodes(ship, name, nodes);
}
export const removeNodes = (
ship: Patp,
name: string,
indices: string[]
): Poke<any> => hookAction({
'remove-nodes': {
resource: { ship, name },
indices
}
});

View File

@ -9,8 +9,8 @@ export interface UrlContent {
}
export interface CodeContent {
code: {
expresssion: string;
output: string | undefined;
expression: string;
output: string[] | undefined;
}
}
@ -47,7 +47,7 @@ export interface GraphChildrenPoke {
}
export interface GraphNode {
children: Graph;
children: Graph | null;
post: Post;
}

View File

@ -1,2 +0,0 @@
export * from './update.d';
export * from './view.d';

View File

@ -1,117 +1,2 @@
import { Enc, Path, Patp, PatpNoSig, Poke } from "..";
import {
Group,
GroupAction,
GroupPolicyDiff,
GroupUpdateAddMembers,
GroupUpdateAddTag,
GroupUpdateChangePolicy,
GroupUpdateRemoveGroup,
GroupUpdateRemoveMembers,
GroupUpdateRemoveTag,
Resource,
Tag
} from "./index.d";
import { GroupPolicy } from "./update";
export const proxyAction = <T>(data: T): Poke<T> => ({
app: 'group-push-hook',
mark: 'group-update',
json: data
});
export const storeAction = <T>(data: T): Poke<T> => ({
app: 'group-store',
mark: 'group-update',
json: data
});
export const remove = (
resource: Resource,
ships: PatpNoSig[]
): Poke<GroupUpdateRemoveMembers> => proxyAction({
removeMembers: {
resource,
ships
}
});
export const addTag = (
resource: Resource,
tag: Tag,
ships: Patp[]
): Poke<GroupUpdateAddTag> => proxyAction({
addTag: {
resource,
tag,
ships
}
});
export const removeTag = (
tag: Tag,
resource: Resource,
ships: PatpNoSig[]
): Poke<GroupUpdateRemoveTag> => proxyAction({
removeTag: {
tag,
resource,
ships
}
});
export const add = (
resource: Resource,
ships: PatpNoSig[]
): Poke<GroupUpdateAddMembers> => proxyAction({
addMembers: {
resource,
ships
}
});
export const removeGroup = (
resource: Resource
): Poke<GroupUpdateRemoveGroup> => storeAction({
removeGroup: {
resource
}
});
export const changePolicy = (
resource: Resource,
diff: GroupPolicyDiff
): Poke<GroupUpdateChangePolicy> => proxyAction({
changePolicy: {
resource,
diff
}
});
export const roleTags = ['janitor', 'moderator', 'admin'];
// TODO make this type better?
export function roleForShip(group: Group, ship: PatpNoSig): string | undefined {
return roleTags.reduce((currRole, role) => {
const roleShips = group?.tags?.role?.[role];
return roleShips && roleShips.has(ship) ? role : currRole;
}, undefined as string | undefined);
}
export function resourceFromPath(path: Path): Resource {
const [, , ship, name] = path.split('/');
return { ship, name }
}
export function makeResource(ship: string, name:string) {
return { ship, name };
}
export const groupBunts = {
group: (): Group => ({ members: new Set(), tags: { role: {} }, hidden: false, policy: groupBunts.policy() }),
policy: (): GroupPolicy => ({ open: { banned: new Set(), banRanks: new Set() } })
};
export const joinError = ['no-perms', 'strange'] as const;
export const joinResult = ['done', ...joinError] as const;
export const joinProgress = ['start', 'added', ...joinResult] as const;
export * from './types';
export * from './lib';

215
pkg/npm/api/groups/lib.ts Normal file
View File

@ -0,0 +1,215 @@
import _ from 'lodash';
import { Enc, Path, Patp, PatpNoSig, Poke, Thread } from '../lib/types';
import { Group, GroupPolicy, GroupPolicyDiff, GroupUpdateAddMembers, GroupUpdateAddTag, GroupUpdateChangePolicy, GroupUpdateRemoveGroup, GroupUpdateRemoveMembers, GroupUpdateRemoveTag, Resource, RoleTags, Tag } from './types';
import { GroupUpdate } from './update';
export const proxyAction = <T>(data: T): Poke<T> => ({
app: 'group-push-hook',
mark: 'group-update',
json: data
});
const storeAction = <T extends GroupUpdate>(data: T): Poke<T> => ({
app: 'group-store',
mark: 'group-update',
json: data
});
export { storeAction as groupStoreAction };
const viewAction = <T>(data: T): Poke<T> => ({
app: 'group-view',
mark: 'group-view-action',
json: data
});
export { viewAction as groupViewAction };
export const viewThread = <T>(thread: string, action: T): Thread<T> => ({
inputMark: 'group-view-action',
outputMark: 'json',
threadName: thread,
body: action
});
export const removeMembers = (
resource: Resource,
ships: PatpNoSig[]
): Poke<GroupUpdateRemoveMembers> => proxyAction({
removeMembers: {
resource,
ships
}
});
export const addTag = (
resource: Resource,
tag: Tag,
ships: Patp[]
): Poke<GroupUpdateAddTag> => proxyAction({
addTag: {
resource,
tag,
ships
}
});
export const removeTag = (
tag: Tag,
resource: Resource,
ships: PatpNoSig[]
): Poke<GroupUpdateRemoveTag> => proxyAction({
removeTag: {
tag,
resource,
ships
}
});
export const addMembers = (
resource: Resource,
ships: PatpNoSig[]
): Poke<GroupUpdateAddMembers> => proxyAction({
addMembers: {
resource,
ships
}
});
export const removeGroup = (
resource: Resource
): Poke<GroupUpdateRemoveGroup> => storeAction({
removeGroup: {
resource
}
});
export const changePolicy = (
resource: Resource,
diff: GroupPolicyDiff
): Poke<GroupUpdateChangePolicy> => proxyAction({
changePolicy: {
resource,
diff
}
});
export const join = (
ship: string,
name: string
): Poke<any> => viewAction({
join: {
resource: makeResource(ship, name),
ship
}
});
export const createGroup = (
name: string,
policy: Enc<GroupPolicy>,
title: string,
description: string
): Thread<any> => viewThread('group-create', {
create: {
name,
policy,
title,
description
}
});
export const deleteGroup = (
ship: string,
name: string
): Thread<any> => viewThread('group-delete', {
remove: makeResource(ship, name)
});
export const leaveGroup = (
ship: string,
name: string
): Thread<any> => viewThread('group-leave', {
leave: makeResource(ship, name)
});
export const invite = (
ship: string,
name: string,
ships: Patp[],
description: string
): Thread<any> => viewThread('group-invite', {
invite: {
resource: makeResource(ship, name),
ships,
description
}
});
export const roleTags = ['janitor', 'moderator', 'admin'];
// TODO make this type better?
export const groupBunts = {
group: (): Group => ({ members: new Set(), tags: { role: {} }, hidden: false, policy: groupBunts.policy() }),
policy: (): GroupPolicy => ({ open: { banned: new Set(), banRanks: new Set() } })
};
export const joinError = ['no-perms', 'strange'] as const;
export const joinResult = ['done', ...joinError] as const;
export const joinProgress = ['start', 'added', ...joinResult] as const;
export const roleForShip = (
group: Group,
ship: PatpNoSig
): RoleTags | undefined => {
return roleTags.reduce((currRole, role) => {
const roleShips = group?.tags?.role?.[role];
return roleShips && roleShips.has(ship) ? role : currRole;
}, undefined as RoleTags | undefined);
}
export const resourceFromPath = (path: Path): Resource => {
const [, , ship, name] = path.split('/');
return { ship, name };
}
export const makeResource = (ship: string, name: string) => {
return { ship, name };
}
export const isWriter = (group: Group, resource: string, ship: string) => {
const writers: Set<string> | undefined = _.get(
group,
['tags', 'graph', resource, 'writers'],
undefined
);
const admins = group?.tags?.role?.admin ?? new Set();
if (_.isUndefined(writers)) {
return true;
} else {
return writers.has(ship) || admins.has(ship);
}
}
export const isChannelAdmin = (
group: Group,
resource: string,
ship: string
): boolean => {
const role = roleForShip(group, ship.slice(1));
return (
isHost(resource, ship) ||
role === 'admin' ||
role === 'moderator'
);
}
export const isHost = (
resource: string,
ship: string
): boolean => {
const [, , host] = resource.split('/');
return ship === host;
}

View File

@ -0,0 +1,2 @@
export * from './update';
export * from './view';

View File

@ -38,33 +38,33 @@ export type OpenPolicyDiff =
| AllowShipsDiff
| BanShipsDiff;
interface AllowRanksDiff {
export interface AllowRanksDiff {
allowRanks: ShipRank[];
}
interface BanRanksDiff {
export interface BanRanksDiff {
banRanks: ShipRank[];
}
interface AllowShipsDiff {
export interface AllowShipsDiff {
allowShips: PatpNoSig[];
}
interface BanShipsDiff {
export interface BanShipsDiff {
banShips: PatpNoSig[];
}
export type InvitePolicyDiff = AddInvitesDiff | RemoveInvitesDiff;
interface AddInvitesDiff {
export interface AddInvitesDiff {
addInvites: PatpNoSig[];
}
interface RemoveInvitesDiff {
export interface RemoveInvitesDiff {
removeInvites: PatpNoSig[];
}
interface ReplacePolicyDiff {
export interface ReplacePolicyDiff {
replace: GroupPolicy;
}
@ -75,7 +75,7 @@ export type GroupPolicyDiff =
export type GroupPolicy = OpenPolicy | InvitePolicy;
interface TaggedShips {
export interface TaggedShips {
[tag: string]: Set<PatpNoSig>;
}
@ -95,11 +95,11 @@ export type Groups = {
[p in Path]: Group;
};
interface GroupUpdateInitial {
export interface GroupUpdateInitial {
initial: Enc<Groups>;
}
interface GroupUpdateAddGroup {
export interface GroupUpdateAddGroup {
addGroup: {
resource: Resource;
policy: Enc<GroupPolicy>;
@ -107,21 +107,21 @@ interface GroupUpdateAddGroup {
};
}
interface GroupUpdateAddMembers {
export interface GroupUpdateAddMembers {
addMembers: {
ships: PatpNoSig[];
resource: Resource;
};
}
interface GroupUpdateRemoveMembers {
export interface GroupUpdateRemoveMembers {
removeMembers: {
ships: PatpNoSig[];
resource: Resource;
};
}
interface GroupUpdateAddTag {
export interface GroupUpdateAddTag {
addTag: {
tag: Tag;
resource: Resource;
@ -129,7 +129,7 @@ interface GroupUpdateAddTag {
};
}
interface GroupUpdateRemoveTag {
export interface GroupUpdateRemoveTag {
removeTag: {
tag: Tag;
resource: Resource;
@ -137,23 +137,23 @@ interface GroupUpdateRemoveTag {
};
}
interface GroupUpdateChangePolicy {
export interface GroupUpdateChangePolicy {
changePolicy: { resource: Resource; diff: GroupPolicyDiff };
}
interface GroupUpdateRemoveGroup {
export interface GroupUpdateRemoveGroup {
removeGroup: {
resource: Resource;
};
}
interface GroupUpdateExpose {
export interface GroupUpdateExpose {
expose: {
resource: Resource;
};
}
interface GroupUpdateInitialGroup {
export interface GroupUpdateInitialGroup {
initialGroup: {
resource: Resource;
group: Enc<Group>;

View File

@ -0,0 +1,2 @@
export * from './types';
export * from './lib';

250
pkg/npm/api/hark/lib.ts Normal file
View File

@ -0,0 +1,250 @@
import f from 'lodash/fp';
import bigInt, { BigInteger } from 'big-integer';
import { Poke } from '../lib/types';
import { GraphNotifDescription, GraphNotificationContents, GraphNotifIndex, IndexedNotification, NotifIndex, Unreads } from './types';
import { decToUd } from '../lib';
import { Association } from '../metadata/types';
export const harkAction = <T>(data: T): Poke<T> => ({
app: 'hark-store',
mark: 'hark-action',
json: data
});
const graphHookAction = <T>(data: T): Poke<T> => ({
app: 'hark-graph-hook',
mark: 'hark-graph-hook-action',
json: data
});
export { graphHookAction as harkGraphHookAction };
const groupHookAction = <T>(data: T): Poke<T> => ({
app: 'hark-group-hook',
mark: 'hark-group-hook-action',
json: data
});
export { groupHookAction as harkGroupHookAction };
export const actOnNotification = (
frond: string,
intTime: BigInteger,
index: NotifIndex
): Poke<unknown> => harkAction({
[frond]: {
time: decToUd(intTime.toString()),
index
}
});
export const getParentIndex = (
idx: GraphNotifIndex,
contents: GraphNotificationContents
): string | undefined => {
const origIndex = contents[0].index.slice(1).split('/');
const ret = (i: string[]) => `/${i.join('/')}`;
switch (idx.description) {
case 'link':
return '/';
case 'comment':
return ret(origIndex.slice(0, 1));
case 'note':
return '/';
case 'mention':
return undefined;
default:
return undefined;
}
}
export const setMentions = (
mentions: boolean
): Poke<unknown> => graphHookAction({
'set-mentions': mentions
});
export const setWatchOnSelf = (
watchSelf: boolean
): Poke<unknown> => graphHookAction({
'set-watch-on-self': watchSelf
});
export const setDoNotDisturb = (
dnd: boolean
): Poke<unknown> => harkAction({
'set-dnd': dnd
});
export const archive = (
time: BigInteger,
index: NotifIndex
): Poke<unknown> => actOnNotification('archive', time, index);
export const read = (
time: BigInteger,
index: NotifIndex
): Poke<unknown> => actOnNotification('read-note', time, index);
export const readIndex = (
index: NotifIndex
): Poke<unknown> => harkAction({
'read-index': index
});
export const unread = (
time: BigInteger,
index: NotifIndex
): Poke<unknown> => actOnNotification('unread-note', time, index);
export const markCountAsRead = (
association: Association,
parent: string,
description: GraphNotifDescription
): Poke<unknown> => harkAction({
'read-count': {
graph: {
graph: association.resource,
group: association.group,
module: association.metadata.module,
description: description,
index: parent
}
}
});
export const markEachAsRead = (
association: Association,
parent: string,
child: string,
description: GraphNotifDescription,
module: string
): Poke<unknown> => harkAction({
'read-each': {
index: {
graph: {
graph: association.resource,
group: association.group,
description: description,
module: module,
index: parent
}
},
target: child
}
});
export const dec = (
index: NotifIndex,
ref: string
): Poke<unknown> => harkAction({
dec: {
index,
ref
}
});
export const seen = () => harkAction({ seen: null });
export const readAll = () => harkAction({ 'read-all': null });
export const ignoreGroup = (
group: string
): Poke<unknown> => groupHookAction({
ignore: group
});
export const ignoreGraph = (
graph: string,
index: string
): Poke<unknown> => graphHookAction({
ignore: {
graph,
index
}
});
export const listenGroup = (
group: string
): Poke<unknown> => groupHookAction({
listen: group
});
export const listenGraph = (
graph: string,
index: string
): Poke<unknown> => graphHookAction({
listen: {
graph,
index
}
});
export const mute = (
notif: IndexedNotification
): Poke<any> | {} => {
if('graph' in notif.index && 'graph' in notif.notification.contents) {
const { index } = notif;
const parentIndex = getParentIndex(index.graph, notif.notification.contents.graph);
if(!parentIndex) {
return {};
}
return ignoreGraph(index.graph.graph, parentIndex);
}
if('group' in notif.index) {
const { group } = notif.index.group;
return ignoreGroup(group);
}
return {};
}
export const unmute = (
notif: IndexedNotification
): Poke<any> | {} => {
if('graph' in notif.index && 'graph' in notif.notification.contents) {
const { index } = notif;
const parentIndex = getParentIndex(index.graph, notif.notification.contents.graph);
if(!parentIndex) {
return {};
}
return listenGraph(index.graph.graph, parentIndex);
}
if('group' in notif.index) {
return listenGroup(notif.index.group.group);
}
return {};
}
export const getLastSeen = (
unreads: Unreads,
path: string,
index: string
): BigInteger | undefined => {
const lastSeenIdx = unreads.graph?.[path]?.[index]?.unreads;
if (!(typeof lastSeenIdx === 'string')) {
return bigInt.zero;
}
return f.flow(f.split('/'), f.last, x => (x ? bigInt(x) : undefined))(
lastSeenIdx
);
}
export const getUnreadCount = (
unreads: Unreads,
path: string,
index: string
): number => {
const graphUnreads = unreads.graph?.[path]?.[index]?.unreads ?? 0;
return typeof graphUnreads === 'number' ? graphUnreads : graphUnreads.size;
}
export const getNotificationCount = (
unreads: Unreads,
path: string
): number => {
const unread = unreads.graph?.[path] || {};
return Object.keys(unread)
.map(index => unread[index]?.notifications || 0)
.reduce(f.add, 0);
}

View File

@ -1,8 +1,8 @@
import { Post } from "../graph/index.d";
import { GroupUpdate } from "../groups/index.d";
import { Post } from "../graph/types";
import { GroupUpdate } from "../groups/types";
import BigIntOrderedMap from "../lib/BigIntOrderedMap";
export type GraphNotifDescription = "link" | "comment" | "note" | "mention";
export type GraphNotifDescription = "link" | "comment" | "note" | "mention" | "message";
export interface UnreadStats {
unreads: Set<string> | number;

View File

@ -1,7 +0,0 @@
export * from './contacts/index.d'
export * from './graph/index.d';
export * from './groups/index.d';
export * from './hark/index.d';
export * from './invite/index.d';
export * from './lib/index.d';
export * from './metadata/index.d';

View File

@ -1,9 +1,3 @@
import BigIntOrderedMap from './lib/BigIntOrderedMap';
export {
BigIntOrderedMap
};
export * from './contacts';
export * from './graph';
export * from './groups';
@ -11,4 +5,5 @@ export * from './hark';
export * from './invite';
export * from './metadata';
export * from './settings';
export * from './index.d';
export * from './lib';
export * from './lib/BigIntOrderedMap';

View File

@ -1,28 +1,2 @@
import { InviteUpdate, InviteUpdateAccept, InviteUpdateDecline } from "./index.d";
import { Poke, Serial } from "..";
export const action = <T extends InviteUpdate>(data: T): Poke<T> => ({
app: 'invite-store',
mark: 'invite-action',
json: data
});
export const accept = (
app: string,
uid: Serial
): Poke<InviteUpdateAccept> => action({
accept: {
term: app,
uid
}
});
export const decline = (
app: string,
uid: Serial
): Poke<InviteUpdateDecline> => action({
decline: {
term: app,
uid
}
});
export * from './types';
export * from './lib';

28
pkg/npm/api/invite/lib.ts Normal file
View File

@ -0,0 +1,28 @@
import { Poke, Serial } from "..";
import { InviteUpdate, InviteUpdateAccept, InviteUpdateDecline } from "./types";
export const inviteAction = <T extends InviteUpdate>(data: T): Poke<T> => ({
app: 'invite-store',
mark: 'invite-action',
json: data
});
export const accept = (
app: string,
uid: Serial
): Poke<InviteUpdateAccept> => inviteAction({
accept: {
term: app,
uid
}
});
export const decline = (
app: string,
uid: Serial
): Poke<InviteUpdateDecline> => inviteAction({
decline: {
term: app,
uid
}
});

View File

@ -1,5 +1,5 @@
import { Serial, PatpNoSig, Path } from '..';
import { Resource } from "../groups/update.d";
import { Resource } from "../groups";
export type InviteUpdate =
InviteUpdateInitial
@ -10,30 +10,30 @@ export type InviteUpdate =
| InviteUpdateAccepted
| InviteUpdateDecline;
interface InviteUpdateAccept {
export interface InviteUpdateAccept {
accept: {
term: string;
uid: Serial;
}
}
interface InviteUpdateInitial {
export interface InviteUpdateInitial {
initial: Invites;
}
interface InviteUpdateCreate {
export interface InviteUpdateCreate {
create: {
term: string;
};
}
interface InviteUpdateDelete {
export interface InviteUpdateDelete {
delete: {
term: string;
};
}
interface InviteUpdateInvite {
export interface InviteUpdateInvite {
invite: {
term: string;
uid: Serial;
@ -41,14 +41,14 @@ interface InviteUpdateInvite {
};
}
interface InviteUpdateAccepted {
export interface InviteUpdateAccepted {
accepted: {
term: string;
uid: Serial;
};
}
interface InviteUpdateDecline {
export interface InviteUpdateDecline {
decline: {
term: string;
uid: Serial;

2
pkg/npm/api/lib/index.ts Normal file
View File

@ -0,0 +1,2 @@
export * from './lib';
export * from './types';

View File

@ -1,23 +1,14 @@
import _ from "lodash";
import f from "lodash/fp";
import bigInt, { BigInteger } from "big-integer";
import { Resource } from "../groups/index.d";
import { Resource } from "../groups/types";
import { Post, GraphNode } from "../graph/types";
const DA_UNIX_EPOCH = bigInt("170141184475152167957503069145530368000"); // `@ud` ~1970.1.1
const DA_SECOND = bigInt("18446744073709551616"); // `@ud` ~s1
/**
* Returns true if an app uses a graph backend
*
* @param {string} app The name of the app
*
* @return {boolean} Whether or not it uses a graph backend
*/
export function appIsGraph(app: string): boolean {
return app === 'publish' || app == 'link';
}
/**
* Given a bigint representing an urbit date, returns a unix timestamp.
*
@ -136,28 +127,49 @@ export function deSig(ship: string): string | null {
}
// trim patps to match dojo, chat-cli
export function cite(ship: string): string {
export function cite(ship: string) {
let patp = ship,
shortened = "";
if (patp === null || patp === "") {
return "";
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}`;
}
export function uxToHex(ux: string) {
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');
return value;
}
export const hexToUx = (hex: string): string => {
const ux = f.flow(
f.chunk(4),
f.map(x => _.dropWhile(x, (y: unknown) => y === 0).join('')),
f.join('.')
)(hex.split(''));
return `0x${ux}`;
};
// encode the string into @ta-safe format, using logic from +wood.
// for example, 'some Chars!' becomes '~.some.~43.hars~21.'
//
@ -209,3 +221,20 @@ export function numToUd(num: number): string {
f.join('.')
)(num.toString())
}
export const buntPost = (): Post => ({
author: '',
contents: [],
hash: null,
index: '',
signatures: [],
'time-sent': 0
});
export function makeNodeMap(posts: Post[]): Record<string, GraphNode> {
const nodes: Record<string, GraphNode> = {};
posts.forEach((p: Post) => {
nodes[String(p.index)] = { children: null, post: p };
});
return nodes;
}

View File

@ -20,7 +20,7 @@ export type Serial = string;
export type Jug<K,V> = Map<K,Set<V>>;
// name of app
export type AppName = 'chat' | 'link' | 'contacts' | 'publish' | 'graph';
export type AppName = 'chat' | 'link' | 'contacts' | 'publish' | 'graph' | 'groups';
export type ShipRank = 'czar' | 'king' | 'duke' | 'earl' | 'pawn';
@ -54,6 +54,11 @@ export interface Poke<Action> {
json: Action;
}
export interface Scry {
app: string;
path: string;
}
export interface Thread<Action> {
inputMark: string;
outputMark: string;

View File

@ -1,43 +1,2 @@
import { AppName, Path, PatpNoSig, Poke } from "..";
import { Association, Metadata, MetadataUpdateAdd, MetadataUpdateUpdate } from './index.d';
export const action = <T>(data: T): Poke<T> => ({
app: 'metadata-hook',
mark: 'metadata-action',
json: data
});
export const add = (
appName: AppName,
resource: string,
group: string,
metadata: Metadata,
): Poke<MetadataUpdateAdd> => {
return action({
add: {
group,
resource: {
resource,
'app-name': appName
},
metadata
}
});
}
export const update = (
association: Association,
newMetadata: Partial<Metadata>
): Poke<MetadataUpdateAdd> => {
const { resource, metadata, group } = association;
return action({
add: {
group,
resource: {
resource,
'app-name': association['app-name'],
},
metadata: {...metadata, ...newMetadata }
}
});
}
export * from './types';
export * from './lib';

View File

@ -0,0 +1,77 @@
import { AppName, Path, Poke, uxToHex, PatpNoSig } from "../lib";
import { Association, Metadata, MetadataUpdate, MetadataUpdateAdd, MetadataUpdateRemove } from './types';
export const metadataAction = <T extends MetadataUpdate>(data: T): Poke<T> => ({
app: 'metadata-push-hook',
mark: 'metadata-update',
json: data
});
export const add = (
ship: PatpNoSig,
appName: AppName,
resource: Path,
group: Path,
title: string,
description: string,
dateCreated: string,
color: string,
moduleName: string,
): Poke<MetadataUpdateAdd> => metadataAction({
add: {
group,
resource: {
resource,
app: appName
},
metadata: {
title,
description,
color,
'date-created': dateCreated,
creator: `~${ship}`,
'module': moduleName,
picture: '',
preview: false,
vip: ''
}
}
});
export { add as metadataAdd };
export const remove = (
appName: AppName,
resource: string,
group: string
): Poke<MetadataUpdateRemove> => metadataAction({
remove: {
group,
resource: {
resource,
app: appName
}
}
});
export { remove as metadataRemove };
export const update = (
association: Association,
newMetadata: Partial<Metadata>
): Poke<MetadataUpdateAdd> => {
const metadata = { ...association.metadata, ...newMetadata };
metadata.color = uxToHex(metadata.color);
return metadataAction({
add: {
group: association.group,
resource: {
resource: association.resource,
app: association.app
},
metadata
}
});
}
export { update as metadataUpdate };

View File

@ -1,4 +1,4 @@
import { AppName, Path, Patp } from '..';
import { AppName, Path, Patp } from "../lib";
export type MetadataUpdate =
MetadataUpdateInitial
@ -6,28 +6,34 @@ export type MetadataUpdate =
| MetadataUpdateUpdate
| MetadataUpdateRemove;
interface MetadataUpdateInitial {
export interface MetadataUpdateInitial {
associations: ResourceAssociations;
}
type ResourceAssociations = {
export type ResourceAssociations = {
[p in Path]: Association;
}
type MetadataUpdateAdd = {
export type MetadataUpdateAdd = {
add: AssociationPoke;
}
type MetadataUpdateUpdate = {
export type MetadataUpdateUpdate = {
update: AssociationPoke;
}
type MetadataUpdateRemove = {
remove: MdResource & {
group: Path;
export type MetadataUpdateRemove = {
remove: {
resource: MdResource;
group: string;
}
}
export interface MdResource {
resource: string;
app: AppName;
}
export interface MetadataUpdatePreview {
group: string;
channels: Associations;
@ -42,10 +48,7 @@ export type AppAssociations = {
[p in Path]: Association;
}
interface MdResource {
resource: Path;
'app-name': AppName;
}
export type Association = MdResource & {
group: Path;

View File

@ -7,15 +7,18 @@
"url": "ssh://git@github.com/urbit/urbit.git",
"directory": "pkg/npm/api"
},
"main": "index.js",
"types": "index.d.ts",
"main": "dist/index.js",
"types": "dist/index.d",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
"test": "echo \"Error: no test specified\" && exit 1",
"build": "npm run clean && tsc -p tsconfig.json",
"clean": "rm -rf dist/*"
},
"author": "",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.12.5",
"@types/lodash": "^4.14.168",
"@urbit/eslint-config": "^1.0.0",
"big-integer": "^1.6.48",
"lodash": "^4.17.20"

0
pkg/npm/api/s3/index.ts Normal file
View File

47
pkg/npm/api/s3/lib.ts Normal file
View File

@ -0,0 +1,47 @@
import { Poke } from '../lib/types';
import { S3Update, S3UpdateAccessKeyId, S3UpdateAddBucket, S3UpdateCurrentBucket, S3UpdateEndpoint, S3UpdateRemoveBucket, S3UpdateSecretAccessKey } from './types';
const s3Action = <T extends S3Update>(
data: any
): Poke<T> => ({
app: 's3-store',
mark: 's3-action',
json: data
});
export const setCurrentBucket = (
bucket: string
): Poke<S3UpdateCurrentBucket> => s3Action({
'set-current-bucket': bucket
});
export const addBucket = (
bucket: string
): Poke<S3UpdateAddBucket> => s3Action({
'add-bucket': bucket
});
export const removeBucket = (
bucket: string
): Poke<S3UpdateRemoveBucket> => s3Action({
'remove-bucket': bucket
});
export const setEndpoint = (
endpoint: string
): Poke<S3UpdateEndpoint> => s3Action({
'set-endpoint': endpoint
});
export const setAccessKeyId = (
accessKeyId: string
): Poke<S3UpdateAccessKeyId> => s3Action({
'set-access-key-id': accessKeyId
});
export const setSecretAccessKey = (
secretAccessKey: string
): Poke<S3UpdateSecretAccessKey> => s3Action({
'set-secret-access-key': secretAccessKey
});

60
pkg/npm/api/s3/types.ts Normal file
View File

@ -0,0 +1,60 @@
export interface S3Credentials {
endpoint: string;
accessKeyId: string;
secretAccessKey: string;
}
export interface S3Configuration {
buckets: Set<string>;
currentBucket: string;
}
export interface S3State {
configuration: S3Configuration;
credentials: S3Credentials | null;
}
export interface S3UpdateCredentials {
credentials: S3Credentials;
}
export interface S3UpdateConfiguration {
configuration: {
buckets: string[];
currentBucket: string;
}
}
export interface S3UpdateCurrentBucket {
setCurrentBucket: string;
}
export interface S3UpdateAddBucket {
addBucket: string;
}
export interface S3UpdateRemoveBucket {
removeBucket: string;
}
export interface S3UpdateEndpoint {
setEndpoint: string;
}
export interface S3UpdateAccessKeyId {
setAccessKeyId: string;
}
export interface S3UpdateSecretAccessKey {
setSecretAccessKey: string;
}
export type S3Update =
S3UpdateCredentials
| S3UpdateConfiguration
| S3UpdateCurrentBucket
| S3UpdateAddBucket
| S3UpdateRemoveBucket
| S3UpdateEndpoint
| S3UpdateAccessKeyId
| S3UpdateSecretAccessKey;

View File

@ -0,0 +1,2 @@
export * from './types';
export * from './lib';

View File

@ -0,0 +1,50 @@
import { Poke } from "../lib";
import { PutBucket, Key, Bucket, DelBucket, Value, PutEntry, DelEntry, SettingsUpdate } from './types';
export const action = <T extends SettingsUpdate>(data: T): Poke<T> => ({
app: 'settings-store',
mark: 'settings-event',
json: data
});
export const putBucket = (
key: Key,
bucket: Bucket
): Poke<PutBucket> => action({
'put-bucket': {
'bucket-key': key,
'bucket': bucket
}
});
export const delBucket = (
key: Key
): Poke<DelBucket> => action({
'del-bucket': {
'bucket-key': key
}
});
export const putEntry = (
bucket: Key,
key: Key,
value: Value
): Poke<PutEntry> => action({
'put-entry': {
'bucket-key': bucket,
'entry-key': key,
value: value
}
});
export const delEntry = (
bucket: Key,
key: Key
): Poke<DelEntry> => action({
'del-entry': {
'bucket-key': bucket,
'entry-key': key
}
});
export * from './types';

View File

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

View File

@ -1,25 +1,19 @@
{
"include": ["*.ts"],
"exclude": ["node_modules", "dist", "@types"],
"compilerOptions": {
"allowSyntheticDefaultImports": true,
"noFallthroughCasesInSwitch": true,
"noUnusedParameters": false,
"noImplicitReturns": true,
"outDir": "./dist",
"module": "ESNext",
"noImplicitAny": true,
"target": "ESNext",
"pretty": true,
"moduleResolution": "node",
"esModuleInterop": true,
"noUnusedLocals": false,
"noImplicitAny": false,
"noEmit": true,
"target": "es2015",
"module": "es2015",
"strict": true,
"jsx": "react",
"baseUrl": ".",
"paths": {
"~/*": ["src/*"]
}
},
"include": [
"**/*"
],
"exclude": [ "node_modules" ]
}
"allowSyntheticDefaultImports": true,
"declaration": true,
"sourceMap": true,
"strict": false,
"noErrorTruncation": true,
"allowJs": true,
}
}

View File

@ -0,0 +1 @@
example/*.js

View File

@ -1,17 +0,0 @@
/*
* ATTENTION: The "eval" devtool has been used (maybe by default in mode: "development").
* This devtool is not neither made for production nor for readable output files.
* It uses "eval()" calls to create a separate source file in the browser devtools.
* If you are trying to read the output file, select a different devtool (https://webpack.js.org/configuration/devtool/)
* or disable the default devtool with "devtool: false".
* If you are looking for production-ready output files, see mode: "production" (https://webpack.js.org/configuration/mode/).
*/
/******/ (() => { // webpackBootstrap
/*!********************************!*\
!*** ./src/example/browser.js ***!
\********************************/
/*! unknown exports (runtime-defined) */
/*! runtime requirements: */
eval("// import Urbit from '../../dist/browser';\n// window.Urbit = Urbit;\n\n//# sourceURL=webpack://@urbit/http-api/./src/example/browser.js?");
/******/ })()
;

View File

@ -1,17 +0,0 @@
/*
* ATTENTION: The "eval" devtool has been used (maybe by default in mode: "development").
* This devtool is not neither made for production nor for readable output files.
* It uses "eval()" calls to create a separate source file in the browser devtools.
* If you are trying to read the output file, select a different devtool (https://webpack.js.org/configuration/devtool/)
* or disable the default devtool with "devtool: false".
* If you are looking for production-ready output files, see mode: "production" (https://webpack.js.org/configuration/mode/).
*/
/******/ (() => { // webpackBootstrap
/*!*****************************!*\
!*** ./src/example/node.js ***!
\*****************************/
/*! unknown exports (runtime-defined) */
/*! runtime requirements: */
eval("// import Urbit from '../../dist/index';\n// async function blastOff() {\n// const airlock = await Urbit.authenticate({\n// ship: 'zod',\n// url: 'localhost:8080',\n// code: 'lidlut-tabwed-pillex-ridrup',\n// verbose: true\n// });\n// airlock.subscribe('chat-view', '/primary');\n// }\n// blastOff();\n\n//# sourceURL=webpack://@urbit/http-api/./src/example/node.js?");
/******/ })()
;

View File

@ -1,2 +0,0 @@
import Urbit from './dist';
export { Urbit as default, Urbit };

View File

@ -8,20 +8,15 @@
"url": "ssh://git@github.com/urbit/urbit.git",
"directory": "pkg/npm/http-api"
},
"main": "dist/cjs/index.js",
"module": "dist/esm/index.js",
"browser": "dist/esm/index.js",
"types": "dist/esm/index.d.ts",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"files": [
"dist",
"src"
],
"engines": {
"node": ">=13"
},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "npm run clean && webpack --config webpack.prod.js && tsc -p tsconfig.json && tsc -p tsconfig-cjs.json",
"build": "npm run clean && tsc -p tsconfig.json",
"clean": "rm -rf dist/*"
},
"peerDependencies": {},
@ -38,31 +33,29 @@
"@babel/plugin-proposal-object-rest-spread": "^7.12.1",
"@babel/plugin-proposal-optional-chaining": "^7.12.1",
"@babel/preset-typescript": "^7.12.1",
"@types/browser-or-node": "^1.2.0",
"@types/eventsource": "^1.1.5",
"@types/react": "^16.9.56",
"@typescript-eslint/eslint-plugin": "^4.7.0",
"@typescript-eslint/parser": "^4.7.0",
"@types/browser-or-node": "^1.2.0",
"babel-loader": "^8.2.1",
"clean-webpack-plugin": "^3.0.0",
"tslib": "^2.0.3",
"typescript": "^3.9.7",
"util": "^0.12.3",
"webpack": "^5.4.0",
"webpack-cli": "^3.3.12",
"webpack-dev-server": "^3.11.0"
},
"dependencies": {
"@babel/runtime": "^7.12.5",
"@microsoft/fetch-event-source": "^2.0.0",
"@urbit/api": "file:../api",
"browser-or-node": "^1.3.0",
"browserify-zlib": "^0.2.0",
"buffer": "^5.7.1",
"encoding": "^0.1.13",
"eventsource": "^1.0.7",
"buffer": "^6.0.3",
"node-fetch": "^2.6.1",
"stream-browserify": "^3.0.0",
"stream-http": "^3.1.1",
"util": "^0.12.3",
"xmlhttprequest": "^1.8.0",
"xmlhttprequest-ssl": "^1.6.0"
"stream-http": "^3.1.1"
}
}

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