mirror of
https://github.com/urbit/shrub.git
synced 2024-12-24 03:14:30 +03:00
Merge branch 'release/next-js' of https://github.com/urbit/urbit into zustand-stores
This commit is contained in:
commit
1685c484e0
10
.github/workflows/merge.yml
vendored
10
.github/workflows/merge.yml
vendored
@ -15,3 +15,13 @@ jobs:
|
|||||||
target_branch: release/next-js
|
target_branch: release/next-js
|
||||||
github_token: ${{ secrets.JANEWAY_BOT_TOKEN }}
|
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
20
.github/workflows/ops-group-timer.yml
vendored
Normal 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 }}
|
||||||
|
|
@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:7bf07220286010c21998e38497be5f2c771219bbcb6dac3e2c7bb0af5dbf4fb8
|
oid sha256:9812a52d34be0d6d47ca60b23d3386e7db296ff61fac7c4b1f33a35806f8cb7c
|
||||||
size 9668893
|
size 9751012
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
/- glob
|
/- glob
|
||||||
/+ default-agent, verb, dbug
|
/+ 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))]
|
+$ state-0 [%0 hash=@uv glob=(unit (each glob:glob tid=@ta))]
|
||||||
+$ all-states
|
+$ all-states
|
||||||
$% state-0
|
$% state-0
|
||||||
|
@ -767,6 +767,8 @@
|
|||||||
++ inflate-cache
|
++ inflate-cache
|
||||||
|= state-4
|
|= state-4
|
||||||
^+ +.state
|
^+ +.state
|
||||||
|
=. +.state
|
||||||
|
*cache
|
||||||
=/ nots=(list [p=@da =timebox:store])
|
=/ nots=(list [p=@da =timebox:store])
|
||||||
(tap:orm notifications)
|
(tap:orm notifications)
|
||||||
|- =* outer $
|
|- =* outer $
|
||||||
|
@ -24,6 +24,6 @@
|
|||||||
<div id="portal-root"></div>
|
<div id="portal-root"></div>
|
||||||
<script src="/~landscape/js/channel.js"></script>
|
<script src="/~landscape/js/channel.js"></script>
|
||||||
<script src="/~landscape/js/session.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>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
@ -4,13 +4,12 @@
|
|||||||
+$ card card:agent:gall
|
+$ card card:agent:gall
|
||||||
+$ versioned-state
|
+$ versioned-state
|
||||||
$% state-0
|
$% state-0
|
||||||
|
state-1
|
||||||
==
|
==
|
||||||
+$ state-0
|
+$ state-0 [%0 settings=settings-0]
|
||||||
$: %0
|
+$ state-1 [%1 =settings]
|
||||||
=settings
|
|
||||||
==
|
|
||||||
--
|
--
|
||||||
=| state-0
|
=| state-1
|
||||||
=* state -
|
=* state -
|
||||||
::
|
::
|
||||||
%- agent:dbug
|
%- agent:dbug
|
||||||
@ -32,8 +31,10 @@
|
|||||||
|= =old=vase
|
|= =old=vase
|
||||||
^- (quip card _this)
|
^- (quip card _this)
|
||||||
=/ old !<(versioned-state old-vase)
|
=/ old !<(versioned-state old-vase)
|
||||||
|
|-
|
||||||
?- -.old
|
?- -.old
|
||||||
%0 [~ this(state old)]
|
%0 $(old [%1 +.old])
|
||||||
|
%1 [~ this(state old)]
|
||||||
==
|
==
|
||||||
::
|
::
|
||||||
++ on-poke
|
++ on-poke
|
||||||
|
@ -50,7 +50,7 @@
|
|||||||
%- pairs
|
%- pairs
|
||||||
:~ bucket-key+s+b
|
:~ bucket-key+s+b
|
||||||
entry-key+s+k
|
entry-key+s+k
|
||||||
value+(val v)
|
value+(value v)
|
||||||
==
|
==
|
||||||
::
|
::
|
||||||
++ del-entry
|
++ del-entry
|
||||||
@ -68,6 +68,7 @@
|
|||||||
%s val
|
%s val
|
||||||
%b val
|
%b val
|
||||||
%n (numb p.val)
|
%n (numb p.val)
|
||||||
|
%a [%a (turn p.val value)]
|
||||||
==
|
==
|
||||||
::
|
::
|
||||||
++ bucket
|
++ bucket
|
||||||
@ -105,7 +106,7 @@
|
|||||||
%- ot
|
%- ot
|
||||||
:~ bucket-key+so
|
:~ bucket-key+so
|
||||||
entry-key+so
|
entry-key+so
|
||||||
value+val
|
value+value
|
||||||
==
|
==
|
||||||
::
|
::
|
||||||
++ del-entry
|
++ del-entry
|
||||||
@ -121,6 +122,7 @@
|
|||||||
%s jon
|
%s jon
|
||||||
%b jon
|
%b jon
|
||||||
%n [%n (rash p.jon dem)]
|
%n [%n (rash p.jon dem)]
|
||||||
|
%a [%a (turn p.jon value)]
|
||||||
==
|
==
|
||||||
::
|
::
|
||||||
++ bucket
|
++ bucket
|
||||||
|
@ -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)
|
+$ settings (map key bucket)
|
||||||
+$ bucket (map key val)
|
+$ bucket (map key val)
|
||||||
+$ key term
|
+$ key term
|
||||||
+$ val
|
+$ val
|
||||||
|
$~ [%n 0]
|
||||||
$% [%s p=@t]
|
$% [%s p=@t]
|
||||||
[%b p=?]
|
[%b p=?]
|
||||||
[%n p=@]
|
[%n p=@]
|
||||||
|
[%a p=(list val)]
|
||||||
==
|
==
|
||||||
+$ event
|
+$ event
|
||||||
$% [%put-bucket =key =bucket]
|
$% [%put-bucket =key =bucket]
|
||||||
|
@ -1217,7 +1217,7 @@
|
|||||||
on-hear-forward
|
on-hear-forward
|
||||||
::
|
::
|
||||||
?: ?& ?=(%pawn (clan:title sndr.packet))
|
?: ?& ?=(%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-open
|
||||||
on-hear-shut
|
on-hear-shut
|
||||||
@ -1289,14 +1289,9 @@
|
|||||||
|= [=lane =packet dud=(unit goof)]
|
|= [=lane =packet dud=(unit goof)]
|
||||||
^+ event-core
|
^+ event-core
|
||||||
=/ sndr-state (~(get by peers.ames-state) sndr.packet)
|
=/ sndr-state (~(get by peers.ames-state) sndr.packet)
|
||||||
:: if we don't know them, maybe enqueue a jael %public-keys request
|
:: if we don't know them, ask jael for their keys and enqueue
|
||||||
::
|
|
||||||
:: Ignore encrypted packets from alien comets.
|
|
||||||
:: TODO: maybe crash?
|
|
||||||
::
|
::
|
||||||
?. ?=([~ %known *] sndr-state)
|
?. ?=([~ %known *] sndr-state)
|
||||||
?: =(%pawn (clan:title sndr.packet))
|
|
||||||
event-core
|
|
||||||
(enqueue-alien-todo sndr.packet |=(alien-agenda +<))
|
(enqueue-alien-todo sndr.packet |=(alien-agenda +<))
|
||||||
:: decrypt packet contents using symmetric-key.channel
|
:: decrypt packet contents using symmetric-key.channel
|
||||||
::
|
::
|
||||||
|
15
pkg/interface/package-lock.json
generated
15
pkg/interface/package-lock.json
generated
@ -1782,31 +1782,26 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": {
|
"@babel/runtime": {
|
||||||
"version": "7.12.5",
|
"version": "7.12.5",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.12.5.tgz",
|
"bundled": true,
|
||||||
"integrity": "sha512-plcc+hbExy3McchJCEQG3knOsuh3HH+Prx1P6cLIkET/0dLuQDEnrT+s27Axgc9bqfsmNUNHfscgMUdBpC9xfg==",
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"regenerator-runtime": "^0.13.4"
|
"regenerator-runtime": "^0.13.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@urbit/eslint-config": {
|
"@urbit/eslint-config": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@urbit/eslint-config/-/eslint-config-1.0.0.tgz",
|
"bundled": true
|
||||||
"integrity": "sha512-Xmzb6MvM7KorlPJEq/hURZZ4BHSVy/7CoQXWogsBSTv5MOZnMqwNKw6yt24k2AO/2UpHwjGptimaNLqFfesJbw=="
|
|
||||||
},
|
},
|
||||||
"big-integer": {
|
"big-integer": {
|
||||||
"version": "1.6.48",
|
"version": "1.6.48",
|
||||||
"resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.48.tgz",
|
"bundled": true
|
||||||
"integrity": "sha512-j51egjPa7/i+RdiRuJbPdJ2FIUYYPhvYLjzoYbcMMm62ooO6F94fETG4MTs46zPAF9Brs04OajboA/qTGuz78w=="
|
|
||||||
},
|
},
|
||||||
"lodash": {
|
"lodash": {
|
||||||
"version": "4.17.20",
|
"version": "4.17.20",
|
||||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz",
|
"bundled": true
|
||||||
"integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA=="
|
|
||||||
},
|
},
|
||||||
"regenerator-runtime": {
|
"regenerator-runtime": {
|
||||||
"version": "0.13.7",
|
"version": "0.13.7",
|
||||||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz",
|
"bundled": true
|
||||||
"integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew=="
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -11,7 +11,7 @@ export default class SettingsApi extends BaseApi<StoreState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
putBucket(key: Key, bucket: Bucket) {
|
putBucket(key: Key, bucket: Bucket) {
|
||||||
this.storeAction({
|
return this.storeAction({
|
||||||
'put-bucket': {
|
'put-bucket': {
|
||||||
'bucket-key': key,
|
'bucket-key': key,
|
||||||
'bucket': bucket
|
'bucket': bucket
|
||||||
@ -20,7 +20,7 @@ export default class SettingsApi extends BaseApi<StoreState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
delBucket(key: Key) {
|
delBucket(key: Key) {
|
||||||
this.storeAction({
|
return this.storeAction({
|
||||||
'del-bucket': {
|
'del-bucket': {
|
||||||
'bucket-key': key
|
'bucket-key': key
|
||||||
}
|
}
|
||||||
@ -38,7 +38,7 @@ export default class SettingsApi extends BaseApi<StoreState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
delEntry(buc: Key, key: Key) {
|
delEntry(buc: Key, key: Key) {
|
||||||
this.storeAction({
|
return this.storeAction({
|
||||||
'put-entry': {
|
'put-entry': {
|
||||||
'bucket-key': buc,
|
'bucket-key': buc,
|
||||||
'entry-key': key
|
'entry-key': key
|
||||||
@ -47,8 +47,10 @@ export default class SettingsApi extends BaseApi<StoreState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getAll() {
|
async getAll() {
|
||||||
const data = await this.scry('settings-store', '/all');
|
const { all } = await this.scry("settings-store", "/all");
|
||||||
this.store.handleEvent({ data: { 'settings-data': data.all } });
|
this.store.handleEvent({data:
|
||||||
|
{"settings-data": { all } }
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async getBucket(bucket: Key) {
|
async getBucket(bucket: Key) {
|
||||||
|
72
pkg/interface/src/logic/lib/migrateSettings.ts
Normal file
72
pkg/interface/src/logic/lib/migrateSettings.ts
Normal 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");
|
||||||
|
};
|
||||||
|
}
|
@ -1,7 +1,7 @@
|
|||||||
import { cite } from '~/logic/lib/util';
|
import { cite } from '~/logic/lib/util';
|
||||||
import { isChannelAdmin } from '~/logic/lib/group';
|
import { isChannelAdmin } from '~/logic/lib/group';
|
||||||
|
|
||||||
const indexes = new Map([
|
const makeIndexes = () => new Map([
|
||||||
['ships', []],
|
['ships', []],
|
||||||
['commands', []],
|
['commands', []],
|
||||||
['subscriptions', []],
|
['subscriptions', []],
|
||||||
@ -70,18 +70,27 @@ const appIndex = function (apps) {
|
|||||||
return applications;
|
return applications;
|
||||||
};
|
};
|
||||||
|
|
||||||
const otherIndex = function() {
|
const otherIndex = function(config) {
|
||||||
const other = [];
|
const other = [];
|
||||||
other.push(result('My Channels', '/~landscape/home', 'home', null));
|
const idx = {
|
||||||
other.push(result('Notifications', '/~notifications', 'inbox', null));
|
mychannel: result('My Channels', '/~landscape/home', 'home', null),
|
||||||
other.push(result('Profile and Settings', `/~profile/~${window.ship}`, 'profile', null));
|
updates: result('Notifications', '/~notifications', 'inbox', null),
|
||||||
other.push(result('Messages', '/~landscape/messages', 'messages', null));
|
profile: result('Profile', `/~profile/~${window.ship}`, 'profile', null),
|
||||||
other.push(result('Log Out', '/~/logout', 'logout', null));
|
messages: result('Messages', '/~landscape/messages', 'messages', null),
|
||||||
|
logout: result('Log Out', '/~/logout', 'logout', null)
|
||||||
|
};
|
||||||
|
|
||||||
|
for(let cat of config.categories) {
|
||||||
|
if(idx[cat]) {
|
||||||
|
other.push(idx[cat]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return other;
|
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));
|
indexes.set('ships', shipIndex(contacts));
|
||||||
// all metadata from all apps is indexed
|
// all metadata from all apps is indexed
|
||||||
// into subscriptions and landscape
|
// into subscriptions and landscape
|
||||||
@ -141,7 +150,7 @@ export default function index(contacts, associations, apps, currentGroup, groups
|
|||||||
indexes.set('subscriptions', subscriptions);
|
indexes.set('subscriptions', subscriptions);
|
||||||
indexes.set('groups', landscape);
|
indexes.set('groups', landscape);
|
||||||
indexes.set('apps', appIndex(apps));
|
indexes.set('apps', appIndex(apps));
|
||||||
indexes.set('other', otherIndex());
|
indexes.set('other', otherIndex(hide));
|
||||||
|
|
||||||
return indexes;
|
return indexes;
|
||||||
};
|
};
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import urbitOb from 'urbit-ob';
|
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) => {
|
const isUrl = (string) => {
|
||||||
try {
|
try {
|
||||||
|
@ -5,6 +5,7 @@ import bigInt, { BigInteger } from 'big-integer';
|
|||||||
import { Association, Contact } from '@urbit/api';
|
import { Association, Contact } from '@urbit/api';
|
||||||
import useLocalState from '../state/local';
|
import useLocalState from '../state/local';
|
||||||
import produce from 'immer';
|
import produce from 'immer';
|
||||||
|
import useSettingsState from '../state/settings';
|
||||||
|
|
||||||
export const MOBILE_BROWSER_REGEX = /Android|webOS|iPhone|iPad|iPod|BlackBerry/i;
|
export const MOBILE_BROWSER_REGEX = /Android|webOS|iPhone|iPad|iPod|BlackBerry/i;
|
||||||
|
|
||||||
@ -377,8 +378,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
|
// Hide is an optional second parameter for when this function is used in class components
|
||||||
export function useShowNickname(contact: Contact | null, hide?: boolean): boolean {
|
export function useShowNickname(contact: Contact | null, hide?: boolean): boolean {
|
||||||
const hideNicknames = typeof hide !== 'undefined' ? hide : useLocalState(state => state.hideNicknames);
|
const hideNicknames = typeof hide !== 'undefined' ? hide : useSettingsState(state => state.calm.hideNicknames);
|
||||||
return Boolean(contact && contact.nickname && !hideNicknames);
|
return !!(contact && contact.nickname && !hideNicknames);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface useHoveringInterface {
|
interface useHoveringInterface {
|
||||||
|
@ -1,77 +1,83 @@
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { StoreState } from '../../store/type';
|
import { SettingsUpdate } from '~/types/settings';
|
||||||
import {
|
import useSettingsState, { SettingsStateZus } from "~/logic/state/settings";
|
||||||
SettingsUpdate
|
import produce from 'immer';
|
||||||
} from '@urbit/api/settings';
|
|
||||||
|
|
||||||
type SettingsState = Pick<StoreState, 'settings'>;
|
export default class SettingsStateZusettingsReducer{
|
||||||
|
reduce(json: any) {
|
||||||
export default class SettingsReducer<S extends SettingsState> {
|
const old = useSettingsState.getState();
|
||||||
reduce(json: Cage, state: S) {
|
const newState = produce(old, state => {
|
||||||
let data = json['settings-event'];
|
let data = json["settings-event"];
|
||||||
if (data) {
|
if (data) {
|
||||||
|
console.log(data);
|
||||||
this.putBucket(data, state);
|
this.putBucket(data, state);
|
||||||
this.delBucket(data, state);
|
this.delBucket(data, state);
|
||||||
this.putEntry(data, state);
|
this.putEntry(data, state);
|
||||||
this.delEntry(data, state);
|
this.delEntry(data, state);
|
||||||
}
|
}
|
||||||
data = json['settings-data'];
|
data = json["settings-data"];
|
||||||
if (data) {
|
if (data) {
|
||||||
|
console.log(data);
|
||||||
this.getAll(data, state);
|
this.getAll(data, state);
|
||||||
this.getBucket(data, state);
|
this.getBucket(data, state);
|
||||||
this.getEntry(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);
|
const data = _.get(json, 'put-bucket', false);
|
||||||
if (data) {
|
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);
|
const data = _.get(json, 'del-bucket', false);
|
||||||
if (data) {
|
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);
|
const data = _.get(json, 'put-entry', false);
|
||||||
if (data) {
|
if (data) {
|
||||||
if (!state.settings[data['bucket-key']]) {
|
if (!state[data["bucket-key"]]) {
|
||||||
state.settings[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);
|
const data = _.get(json, 'del-entry', false);
|
||||||
if (data) {
|
if (data) {
|
||||||
delete state.settings[data['bucket-key']][data['entry-key']];
|
delete state[data["bucket-key"]][data["entry-key"]];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getAll(json: any, state: S) {
|
getAll(json: any, state: SettingsStateZus) {
|
||||||
state.settings = json;
|
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 key = _.get(json, 'bucket-key', false);
|
||||||
const bucket = _.get(json, 'bucket', false);
|
const bucket = _.get(json, 'bucket', false);
|
||||||
if (key && bucket) {
|
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 bucketKey = _.get(json, 'bucket-key', false);
|
||||||
const entryKey = _.get(json, 'entry-key', false);
|
const entryKey = _.get(json, 'entry-key', false);
|
||||||
const entry = _.get(json, 'entry', false);
|
const entry = _.get(json, 'entry', false);
|
||||||
if (bucketKey && entryKey && entry) {
|
if (bucketKey && entryKey && entry) {
|
||||||
state.settings[bucketKey][entryKey] = entry;
|
state[bucketKey][entryKey] = entry;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,17 +3,20 @@ import f from 'lodash/fp';
|
|||||||
import create, { State } from 'zustand';
|
import create, { State } from 'zustand';
|
||||||
import { persist } from 'zustand/middleware';
|
import { persist } from 'zustand/middleware';
|
||||||
import produce from 'immer';
|
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;
|
hideAvatars: boolean;
|
||||||
hideNicknames: boolean;
|
hideNicknames: boolean;
|
||||||
remoteContentPolicy: RemoteContentPolicy;
|
remoteContentPolicy: RemoteContentPolicy;
|
||||||
tutorialProgress: TutorialProgress;
|
tutorialProgress: TutorialProgress;
|
||||||
|
hideGroups: boolean;
|
||||||
tutorialRef: HTMLElement | null,
|
tutorialRef: HTMLElement | null,
|
||||||
hideTutorial: () => void;
|
hideTutorial: () => void;
|
||||||
nextTutStep: () => void;
|
nextTutStep: () => void;
|
||||||
prevTutStep: () => void;
|
prevTutStep: () => void;
|
||||||
|
hideLeapCats: LeapCategories[];
|
||||||
setTutorialRef: (el: HTMLElement | null) => void;
|
setTutorialRef: (el: HTMLElement | null) => void;
|
||||||
dark: boolean;
|
dark: boolean;
|
||||||
background: BackgroundConfig;
|
background: BackgroundConfig;
|
||||||
@ -21,15 +24,20 @@ export interface LocalState extends State {
|
|||||||
suspendedFocus?: HTMLElement;
|
suspendedFocus?: HTMLElement;
|
||||||
toggleOmnibox: () => void;
|
toggleOmnibox: () => void;
|
||||||
set: (fn: (state: LocalState) => void) => void
|
set: (fn: (state: LocalState) => void) => void
|
||||||
}
|
};
|
||||||
|
|
||||||
|
type LocalStateZus = LocalState & State;
|
||||||
|
|
||||||
export const selectLocalState =
|
export const selectLocalState =
|
||||||
<K extends keyof LocalState>(keys: K[]) => f.pick<LocalState, K>(keys);
|
<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,
|
dark: false,
|
||||||
background: undefined,
|
background: undefined,
|
||||||
hideAvatars: false,
|
hideAvatars: false,
|
||||||
hideNicknames: false,
|
hideNicknames: false,
|
||||||
|
hideLeapCats: [],
|
||||||
|
hideGroups: false,
|
||||||
tutorialProgress: 'hidden',
|
tutorialProgress: 'hidden',
|
||||||
tutorialRef: null,
|
tutorialRef: null,
|
||||||
setTutorialRef: (el: HTMLElement | null) => set(produce((state) => {
|
setTutorialRef: (el: HTMLElement | null) => set(produce((state) => {
|
||||||
|
68
pkg/interface/src/logic/state/settings.tsx
Normal file
68
pkg/interface/src/logic/state/settings.tsx
Normal 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 };
|
@ -111,7 +111,7 @@ export default class GlobalStore extends BaseStore<StoreState> {
|
|||||||
GraphReducer(data);
|
GraphReducer(data);
|
||||||
HarkReducer(data);
|
HarkReducer(data);
|
||||||
ContactReducer(data);
|
ContactReducer(data);
|
||||||
this.settingsReducer.reduce(data, this.state);
|
this.settingsReducer.reduce(data);
|
||||||
GroupViewReducer(data, this.state);
|
GroupViewReducer(data, this.state);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,9 @@
|
|||||||
export const tutorialProgress = ['hidden', 'start', 'group-desc', 'channels', 'chat', 'link', 'publish', 'profile', 'leap', 'notifications', 'done', 'exit'] as const;
|
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];
|
export type TutorialProgress = typeof tutorialProgress[number];
|
||||||
interface LocalUpdateSetDark {
|
interface LocalUpdateSetDark {
|
||||||
setDark: boolean;
|
setDark: boolean;
|
||||||
|
@ -30,19 +30,20 @@ import { foregroundFromBackground } from '~/logic/lib/sigil';
|
|||||||
import { withLocalState } from '~/logic/state/local';
|
import { withLocalState } from '~/logic/state/local';
|
||||||
import { withContactState } from '~/logic/state/contacts';
|
import { withContactState } from '~/logic/state/contacts';
|
||||||
import { withGroupState } from '~/logic/state/groups';
|
import { withGroupState } from '~/logic/state/groups';
|
||||||
|
import { withSettingsState } from '~/logic/state/settings';
|
||||||
|
|
||||||
|
|
||||||
const Root = styled.div`
|
const Root = withSettingsState(styled.div`
|
||||||
font-family: ${p => p.theme.fonts.sans};
|
font-family: ${p => p.theme.fonts.sans};
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
${p => p.background?.type === 'url' ? `
|
${p => p.display.backgroundType === 'url' ? `
|
||||||
background-image: url('${p.background?.url}');
|
background-image: url('${p.display.background}');
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
` : p.background?.type === 'color' ? `
|
` : p.display.backgroundType === 'color' ? `
|
||||||
background-color: ${p.background.color};
|
background-color: ${p.display.background};
|
||||||
` : `background-color: ${p.theme.colors.white};`
|
` : `background-color: ${p.theme.colors.white};`
|
||||||
}
|
}
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -66,7 +67,7 @@ const Root = styled.div`
|
|||||||
border-radius: 1rem;
|
border-radius: 1rem;
|
||||||
border: 0px solid transparent;
|
border: 0px solid transparent;
|
||||||
}
|
}
|
||||||
`;
|
`, ['display']);
|
||||||
|
|
||||||
const StatusBarWithRouter = withRouter(StatusBar);
|
const StatusBarWithRouter = withRouter(StatusBar);
|
||||||
|
|
||||||
@ -148,7 +149,7 @@ class App extends React.Component {
|
|||||||
? <link rel="icon" type="image/svg+xml" href={this.faviconString()} />
|
? <link rel="icon" type="image/svg+xml" href={this.faviconString()} />
|
||||||
: null}
|
: null}
|
||||||
</Helmet>
|
</Helmet>
|
||||||
<Root background={background}>
|
<Root>
|
||||||
<Router>
|
<Router>
|
||||||
<TutorialModal api={this.api} />
|
<TutorialModal api={this.api} />
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
|
@ -16,6 +16,7 @@ import {
|
|||||||
cite,
|
cite,
|
||||||
writeText,
|
writeText,
|
||||||
useShowNickname,
|
useShowNickname,
|
||||||
|
useHideAvatar,
|
||||||
useHovering
|
useHovering
|
||||||
} from '~/logic/lib/util';
|
} from '~/logic/lib/util';
|
||||||
import {
|
import {
|
||||||
@ -32,6 +33,7 @@ import RemoteContent from '~/views/components/RemoteContent';
|
|||||||
import { Mention } from '~/views/components/MentionText';
|
import { Mention } from '~/views/components/MentionText';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import useLocalState from '~/logic/state/local';
|
import useLocalState from '~/logic/state/local';
|
||||||
|
import useSettingsState, {selectCalmState} from "~/logic/state/settings";
|
||||||
import Timestamp from '~/views/components/Timestamp';
|
import Timestamp from '~/views/components/Timestamp';
|
||||||
import useContactState from '~/logic/state/contacts';
|
import useContactState from '~/logic/state/contacts';
|
||||||
|
|
||||||
@ -230,7 +232,7 @@ export const MessageAuthor = ({
|
|||||||
const contact =
|
const contact =
|
||||||
`~${msg.author}` in contacts ? contacts[`~${msg.author}`] : false;
|
`~${msg.author}` in contacts ? contacts[`~${msg.author}`] : false;
|
||||||
const showNickname = useShowNickname(contact);
|
const showNickname = useShowNickname(contact);
|
||||||
const { hideAvatars } = useLocalState(({ hideAvatars }) => ({ hideAvatars }));
|
const { hideAvatars } = useSettingsState(selectCalmState);
|
||||||
const shipName = showNickname ? contact.nickname : cite(msg.author);
|
const shipName = showNickname ? contact.nickname : cite(msg.author);
|
||||||
const copyNotice = 'Copied';
|
const copyNotice = 'Copied';
|
||||||
const color = contact
|
const color = contact
|
||||||
|
@ -30,6 +30,8 @@ import {
|
|||||||
TUTORIAL_CHAT,
|
TUTORIAL_CHAT,
|
||||||
TUTORIAL_LINKS
|
TUTORIAL_LINKS
|
||||||
} from '~/logic/lib/tutorialModal';
|
} from '~/logic/lib/tutorialModal';
|
||||||
|
import useSettingsState, { selectCalmState } from '~/logic/state/settings';
|
||||||
|
|
||||||
|
|
||||||
const ScrollbarLessBox = styled(Box)`
|
const ScrollbarLessBox = styled(Box)`
|
||||||
scrollbar-width: none !important;
|
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) {
|
export default function LaunchApp(props) {
|
||||||
const [hashText, setHashText] = useState(props.baseHash);
|
const [hashText, setHashText] = useState(props.baseHash);
|
||||||
@ -82,6 +84,8 @@ export default function LaunchApp(props) {
|
|||||||
}, [query]);
|
}, [query]);
|
||||||
|
|
||||||
const { tutorialProgress, nextTutStep } = useLocalState(tutSelector);
|
const { tutorialProgress, nextTutStep } = useLocalState(tutSelector);
|
||||||
|
let { hideGroups } = useLocalState(tutSelector);
|
||||||
|
!hideGroups ? { hideGroups } = useSettingsState(selectCalmState) : null;
|
||||||
|
|
||||||
const waiter = useWaitForProps(props);
|
const waiter = useWaitForProps(props);
|
||||||
|
|
||||||
@ -196,8 +200,9 @@ export default function LaunchApp(props) {
|
|||||||
>
|
>
|
||||||
<JoinGroup {...props} />
|
<JoinGroup {...props} />
|
||||||
</ModalButton>
|
</ModalButton>
|
||||||
|
{!hideGroups &&
|
||||||
<Groups />
|
(<Groups />)
|
||||||
|
}
|
||||||
</Box>
|
</Box>
|
||||||
<Box alignSelf="flex-start" display={["block", "none"]}>{hashBox}</Box>
|
<Box alignSelf="flex-start" display={["block", "none"]}>{hashBox}</Box>
|
||||||
</ScrollbarLessBox>
|
</ScrollbarLessBox>
|
||||||
|
@ -12,6 +12,7 @@ import { TUTORIAL_HOST, TUTORIAL_GROUP } from '~/logic/lib/tutorialModal';
|
|||||||
import useGroupState from '~/logic/state/groups';
|
import useGroupState from '~/logic/state/groups';
|
||||||
import useHarkState from '~/logic/state/hark';
|
import useHarkState from '~/logic/state/hark';
|
||||||
import useMetadataState from '~/logic/state/metadata';
|
import useMetadataState from '~/logic/state/metadata';
|
||||||
|
import useSettingsState, { selectCalmState } from '~/logic/state/settings';
|
||||||
|
|
||||||
interface GroupsProps {}
|
interface GroupsProps {}
|
||||||
|
|
||||||
@ -84,11 +85,12 @@ function Group(props: GroupProps) {
|
|||||||
isTutorialGroup,
|
isTutorialGroup,
|
||||||
anchorRef.current
|
anchorRef.current
|
||||||
);
|
);
|
||||||
|
const { hideUnreads } = useSettingsState(selectCalmState)
|
||||||
return (
|
return (
|
||||||
<Tile ref={anchorRef} position="relative" bg={isTutorialGroup ? 'lightBlue' : undefined} to={`/~landscape${path}`} gridColumnStart={first ? '1' : null}>
|
<Tile ref={anchorRef} position="relative" bg={isTutorialGroup ? 'lightBlue' : undefined} to={`/~landscape${path}`} gridColumnStart={first ? '1' : null}>
|
||||||
<Col height="100%" justifyContent="space-between">
|
<Col height="100%" justifyContent="space-between">
|
||||||
<Text>{title}</Text>
|
<Text>{title}</Text>
|
||||||
<Col>
|
{!hideUnreads && (<Col>
|
||||||
{updates > 0 &&
|
{updates > 0 &&
|
||||||
(<Text mt="1" color="blue">{updates} update{updates !== 1 && 's'} </Text>)
|
(<Text mt="1" color="blue">{updates} update{updates !== 1 && 's'} </Text>)
|
||||||
}
|
}
|
||||||
@ -96,7 +98,7 @@ function Group(props: GroupProps) {
|
|||||||
(<Text color="lightGray">{unreads}</Text>)
|
(<Text color="lightGray">{unreads}</Text>)
|
||||||
}
|
}
|
||||||
</Col>
|
</Col>
|
||||||
|
)}
|
||||||
</Col>
|
</Col>
|
||||||
</Tile>
|
</Tile>
|
||||||
);
|
);
|
||||||
|
@ -32,12 +32,10 @@ export default function NotificationPreferences(
|
|||||||
mentions: graphConfig.mentions,
|
mentions: graphConfig.mentions,
|
||||||
watchOnSelf: graphConfig.watchOnSelf,
|
watchOnSelf: graphConfig.watchOnSelf,
|
||||||
dnd,
|
dnd,
|
||||||
watching: graphConfig.watching
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const onSubmit = useCallback(
|
const onSubmit = useCallback(
|
||||||
async (values: FormSchema, actions: FormikHelpers<FormSchema>) => {
|
async (values: FormSchema, actions: FormikHelpers<FormSchema>) => {
|
||||||
console.log(values);
|
|
||||||
try {
|
try {
|
||||||
const promises: Promise<any>[] = [];
|
const promises: Promise<any>[] = [];
|
||||||
if (values.mentions !== graphConfig.mentions) {
|
if (values.mentions !== graphConfig.mentions) {
|
||||||
|
@ -10,7 +10,7 @@ import {
|
|||||||
} from "@tlon/indigo-react";
|
} from "@tlon/indigo-react";
|
||||||
|
|
||||||
import RichText from '~/views/components/RichText'
|
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 { Sigil } from '~/logic/lib/sigil';
|
||||||
import { ViewProfile } from './ViewProfile';
|
import { ViewProfile } from './ViewProfile';
|
||||||
import { EditProfile } from './EditProfile';
|
import { EditProfile } from './EditProfile';
|
||||||
@ -19,10 +19,10 @@ import { uxToHex } from '~/logic/lib/util';
|
|||||||
import { useTutorialModal } from '~/views/components/useTutorialModal';
|
import { useTutorialModal } from '~/views/components/useTutorialModal';
|
||||||
import useContactState from '~/logic/state/contacts';
|
import useContactState from '~/logic/state/contacts';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export function Profile(props: any): ReactElement {
|
export function Profile(props: any): ReactElement {
|
||||||
const { hideAvatars } = useLocalState(({ hideAvatars }) => ({
|
const { hideAvatars } = useSettingsState(selectCalmState);
|
||||||
hideAvatars
|
|
||||||
}));
|
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const nackedContacts = useContactState(state => state.nackedContacts);
|
const nackedContacts = useContactState(state => state.nackedContacts);
|
||||||
|
|
||||||
|
@ -8,7 +8,8 @@ import {
|
|||||||
Text,
|
Text,
|
||||||
Row,
|
Row,
|
||||||
Col,
|
Col,
|
||||||
} from '@tlon/indigo-react';
|
} from "@tlon/indigo-react";
|
||||||
|
import useSettingsState, {selectCalmState} from "~/logic/state/settings";
|
||||||
|
|
||||||
import RichText from '~/views/components/RichText';
|
import RichText from '~/views/components/RichText';
|
||||||
import { GroupLink } from '~/views/components/GroupLink';
|
import { GroupLink } from '~/views/components/GroupLink';
|
||||||
@ -17,9 +18,7 @@ import useLocalState from '~/logic/state/local';
|
|||||||
import useContactState from '~/logic/state/contacts';
|
import useContactState from '~/logic/state/contacts';
|
||||||
|
|
||||||
export function ViewProfile(props: any): ReactElement {
|
export function ViewProfile(props: any): ReactElement {
|
||||||
const { hideNicknames } = useLocalState(({ hideNicknames }) => ({
|
const { hideNicknames } = useSettingsState(selectCalmState);
|
||||||
hideNicknames
|
|
||||||
}));
|
|
||||||
const { api, contact, nacked, ship } = props;
|
const { api, contact, nacked, ship } = props;
|
||||||
|
|
||||||
const isPublic = useContactState(state => state.isContactPublic);
|
const isPublic = useContactState(state => state.isContactPublic);
|
||||||
|
@ -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 fontSize="2" fontWeight="medium">{"<- Back to System Preferences"}</Text>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
@ -1,6 +1,8 @@
|
|||||||
import React, { ReactElement } from 'react';
|
import React, { ReactElement } from 'react';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
Box,
|
||||||
|
Text,
|
||||||
Row,
|
Row,
|
||||||
Label,
|
Label,
|
||||||
Col,
|
Col,
|
||||||
@ -24,30 +26,37 @@ export function BackgroundPicker({
|
|||||||
api: GlobalApi;
|
api: GlobalApi;
|
||||||
}): ReactElement {
|
}): ReactElement {
|
||||||
const rowSpace = { my: 0, alignItems: 'center' };
|
const rowSpace = { my: 0, alignItems: 'center' };
|
||||||
const radioProps = { my: 4, mr: 4, name: 'bgType' };
|
const colProps = { my: 3, mr: 4, gapY: 1 };
|
||||||
return (
|
return (
|
||||||
<Col>
|
<Col>
|
||||||
<Label mb="2">Landscape Background</Label>
|
<Label>Landscape Background</Label>
|
||||||
<Row flexWrap="wrap" {...rowSpace}>
|
<Row flexWrap="wrap" {...rowSpace}>
|
||||||
<Radio {...radioProps} label="Image" id="url" />
|
<Col {...colProps}>
|
||||||
{bgType === 'url' && (
|
<Radio mb="1" name="bgType" label="Image" id="url" />
|
||||||
|
<Text ml="5" gray>Set an image background</Text>
|
||||||
<ImageInput
|
<ImageInput
|
||||||
ml="3"
|
ml="5"
|
||||||
api={api}
|
api={api}
|
||||||
id="bgUrl"
|
id="bgUrl"
|
||||||
|
placeholder="Drop or upload a file, or paste a link here"
|
||||||
name="bgUrl"
|
name="bgUrl"
|
||||||
label="URL"
|
url={bgUrl || ""}
|
||||||
url={bgUrl || ''}
|
|
||||||
/>
|
/>
|
||||||
)}
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
<Row {...rowSpace}>
|
<Row {...rowSpace}>
|
||||||
<Radio label="Color" id="color" {...radioProps} />
|
<Col {...colProps}>
|
||||||
{bgType === 'color' && (
|
<Radio mb="1" label="Color" id="color" name="bgType" />
|
||||||
<ColorInput id="bgColor" label="Color" />
|
<Text ml="5" gray>Set a hex-based background</Text>
|
||||||
)}
|
<ColorInput placeholder="FFFFFF" ml="5" id="bgColor" />
|
||||||
|
</Col>
|
||||||
</Row>
|
</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>
|
</Col>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import React, { ReactElement, useCallback } from 'react';
|
import React, { ReactElement, useCallback, useState } from "react";
|
||||||
import { Formik } from 'formik';
|
import { Formik, FormikHelpers } from 'formik';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ManagedTextInputField as Input,
|
ManagedTextInputField as Input,
|
||||||
@ -10,8 +10,9 @@ import {
|
|||||||
Menu,
|
Menu,
|
||||||
MenuButton,
|
MenuButton,
|
||||||
MenuList,
|
MenuList,
|
||||||
MenuItem
|
MenuItem,
|
||||||
} from '@tlon/indigo-react';
|
Row,
|
||||||
|
} from "@tlon/indigo-react";
|
||||||
|
|
||||||
import GlobalApi from '~/logic/api/global';
|
import GlobalApi from '~/logic/api/global';
|
||||||
|
|
||||||
@ -26,9 +27,12 @@ export function BucketList({
|
|||||||
}): ReactElement {
|
}): ReactElement {
|
||||||
const _buckets = Array.from(buckets);
|
const _buckets = Array.from(buckets);
|
||||||
|
|
||||||
|
const [adding, setAdding] = useState(false);
|
||||||
|
|
||||||
const onSubmit = useCallback(
|
const onSubmit = useCallback(
|
||||||
(values: { newBucket: string }) => {
|
(values: { newBucket: string }, actions: FormikHelpers<any>) => {
|
||||||
api.s3.addBucket(values.newBucket);
|
api.s3.addBucket(values.newBucket);
|
||||||
|
actions.resetForm({ values: { newBucket: "" } });
|
||||||
},
|
},
|
||||||
[api]
|
[api]
|
||||||
);
|
);
|
||||||
@ -67,7 +71,7 @@ export function BucketList({
|
|||||||
alignItems="center"
|
alignItems="center"
|
||||||
borderRadius={1}
|
borderRadius={1}
|
||||||
border={1}
|
border={1}
|
||||||
borderColor="washedGray"
|
borderColor="lightGray"
|
||||||
fontSize={1}
|
fontSize={1}
|
||||||
pl={2}
|
pl={2}
|
||||||
mb={2}
|
mb={2}
|
||||||
@ -91,10 +95,27 @@ export function BucketList({
|
|||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
))}
|
))}
|
||||||
<Input mt="2" label="New Bucket" id="newBucket" />
|
{adding && (
|
||||||
<Button mt="2" style={{ cursor: 'pointer' }} borderColor="washedGray" type="submit">
|
<Input
|
||||||
Add
|
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>
|
||||||
|
<Button
|
||||||
|
width="fit-content"
|
||||||
|
primary
|
||||||
|
type={adding ? "submit" : "button"}
|
||||||
|
onClick={() => setAdding((s) => !s)}
|
||||||
|
>
|
||||||
|
{adding ? "Submit" : "Add new bucket"}
|
||||||
|
</Button>
|
||||||
|
</Row>
|
||||||
</Form>
|
</Form>
|
||||||
</Formik>
|
</Formik>
|
||||||
);
|
);
|
||||||
|
@ -0,0 +1,137 @@
|
|||||||
|
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">
|
||||||
|
<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>
|
||||||
|
);
|
||||||
|
}
|
@ -1,113 +1,108 @@
|
|||||||
import React from 'react';
|
import React from "react";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Box,
|
Col,
|
||||||
ManagedCheckboxField as Checkbox,
|
Text,
|
||||||
Button
|
} from "@tlon/indigo-react";
|
||||||
} from '@tlon/indigo-react';
|
import { Formik, Form } from "formik";
|
||||||
import { Formik, Form } from 'formik';
|
import * as Yup from "yup";
|
||||||
import * as Yup from 'yup';
|
|
||||||
|
|
||||||
import GlobalApi from '~/logic/api/global';
|
import GlobalApi from "~/logic/api/global";
|
||||||
import { uxToHex } from '~/logic/lib/util';
|
import { uxToHex } from "~/logic/lib/util";
|
||||||
import { S3State, BackgroundConfig } from '@urbit/api';
|
import { S3State, BackgroundConfig } from "~/types";
|
||||||
import { BackgroundPicker, BgType } from './BackgroundPicker';
|
import { BackgroundPicker, BgType } from "./BackgroundPicker";
|
||||||
import useLocalState, { LocalState } from '~/logic/state/local';
|
import useSettingsState, { SettingsState, selectSettingsState } from "~/logic/state/settings";
|
||||||
|
import {AsyncButton} from "~/views/components/AsyncButton";
|
||||||
|
|
||||||
const formSchema = Yup.object().shape({
|
const formSchema = Yup.object().shape({
|
||||||
bgType: Yup.string()
|
bgType: Yup.string()
|
||||||
.oneOf(['none', 'color', 'url'], 'invalid')
|
.oneOf(["none", "color", "url"], "invalid")
|
||||||
.required('Required'),
|
.required("Required"),
|
||||||
bgUrl: Yup.string().url(),
|
background: Yup.string(),
|
||||||
bgColor: Yup.string(),
|
|
||||||
avatars: Yup.boolean(),
|
|
||||||
nicknames: Yup.boolean()
|
|
||||||
});
|
});
|
||||||
|
|
||||||
interface FormSchema {
|
interface FormSchema {
|
||||||
bgType: BgType;
|
bgType: BgType;
|
||||||
bgColor: string | undefined;
|
bgColor: string | undefined;
|
||||||
bgUrl: string | undefined;
|
bgUrl: string | undefined;
|
||||||
avatars: boolean;
|
|
||||||
nicknames: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DisplayFormProps {
|
interface DisplayFormProps {
|
||||||
api: GlobalApi;
|
api: GlobalApi;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const settingsSel = selectSettingsState(["display"]);
|
||||||
|
|
||||||
export default function DisplayForm(props: DisplayFormProps) {
|
export default function DisplayForm(props: DisplayFormProps) {
|
||||||
const { api } = props;
|
const { api } = props;
|
||||||
|
|
||||||
const { hideAvatars, hideNicknames, background, set: setLocalState } = useLocalState();
|
const {
|
||||||
|
display: {
|
||||||
|
background,
|
||||||
|
backgroundType,
|
||||||
|
}
|
||||||
|
} = useSettingsState(settingsSel);
|
||||||
|
|
||||||
|
|
||||||
let bgColor, bgUrl;
|
let bgColor, bgUrl;
|
||||||
if (background?.type === 'url') {
|
if (backgroundType === "url") {
|
||||||
bgUrl = background.url;
|
bgUrl = background;
|
||||||
}
|
}
|
||||||
if (background?.type === 'color') {
|
if (backgroundType === "color") {
|
||||||
bgColor = background.color;
|
bgColor = background;
|
||||||
}
|
}
|
||||||
const bgType = background?.type || 'none';
|
const bgType = backgroundType || "none";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Formik
|
<Formik
|
||||||
validationSchema={formSchema}
|
validationSchema={formSchema}
|
||||||
initialValues={
|
initialValues={
|
||||||
{
|
{
|
||||||
bgType,
|
bgType: backgroundType,
|
||||||
bgColor: bgColor || '',
|
bgColor: bgColor || "",
|
||||||
bgUrl,
|
bgUrl
|
||||||
avatars: hideAvatars,
|
|
||||||
nicknames: hideNicknames
|
|
||||||
} as FormSchema
|
} as FormSchema
|
||||||
}
|
}
|
||||||
onSubmit={(values, actions) => {
|
onSubmit={async (values, actions) => {
|
||||||
const bgConfig: BackgroundConfig =
|
let promises = [] as Promise<any>[];
|
||||||
values.bgType === 'color'
|
promises.push(api.settings.putEntry('display', 'backgroundType', values.bgType));
|
||||||
? { type: 'color', color: `#${uxToHex(values.bgColor || '0x0')}` }
|
|
||||||
: values.bgType === 'url'
|
promises.push(
|
||||||
? { type: 'url', url: values.bgUrl || '' }
|
api.settings.putEntry('display', 'background',
|
||||||
: undefined;
|
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>
|
<Form>
|
||||||
<Box
|
<Col p="5" pt="4" gapY="5">
|
||||||
display="grid"
|
<Col gapY="2">
|
||||||
gridTemplateColumns="100%"
|
<Text color="black" fontSize={2} fontWeight="medium">
|
||||||
gridTemplateRows="auto"
|
|
||||||
gridRowGap={5}
|
|
||||||
>
|
|
||||||
<Box color="black" fontSize={1} mb={3} fontWeight={900}>
|
|
||||||
Display Preferences
|
Display Preferences
|
||||||
</Box>
|
</Text>
|
||||||
|
<Text gray>
|
||||||
|
Customize visual interfaces across your Landscape
|
||||||
|
</Text>
|
||||||
|
</Col>
|
||||||
<BackgroundPicker
|
<BackgroundPicker
|
||||||
bgType={props.values.bgType}
|
bgType={props.values.bgType}
|
||||||
bgUrl={props.values.bgUrl}
|
bgUrl={props.values.bgUrl}
|
||||||
api={api}
|
api={api}
|
||||||
/>
|
/>
|
||||||
<Checkbox
|
<AsyncButton primary width="fit-content" type="submit">
|
||||||
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">
|
|
||||||
Save
|
Save
|
||||||
</Button>
|
</AsyncButton>
|
||||||
</Box>
|
</Col>
|
||||||
</Form>
|
</Form>
|
||||||
)}
|
)}
|
||||||
</Formik>
|
</Formik>
|
||||||
|
@ -0,0 +1,100 @@
|
|||||||
|
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">
|
||||||
|
<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>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,87 @@
|
|||||||
|
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">
|
||||||
|
<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>
|
||||||
|
);
|
||||||
|
}
|
@ -5,12 +5,14 @@ import {
|
|||||||
ManagedTextInputField as Input,
|
ManagedTextInputField as Input,
|
||||||
ManagedForm as Form,
|
ManagedForm as Form,
|
||||||
Box,
|
Box,
|
||||||
|
Text,
|
||||||
Button,
|
Button,
|
||||||
Col
|
Col,
|
||||||
|
Anchor
|
||||||
} from '@tlon/indigo-react';
|
} 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';
|
import { S3State } from '~/types/s3-update';
|
||||||
import useS3State from '~/logic/state/s3';
|
import useS3State from '~/logic/state/s3';
|
||||||
|
|
||||||
@ -48,7 +50,7 @@ export default function S3Form(props: S3FormProps): ReactElement {
|
|||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Col>
|
<Col p="5" pt="4" borderBottom="1" borderBottomColor="washedGray">
|
||||||
<Formik
|
<Formik
|
||||||
initialValues={
|
initialValues={
|
||||||
{
|
{
|
||||||
@ -61,15 +63,25 @@ export default function S3Form(props: S3FormProps): ReactElement {
|
|||||||
}
|
}
|
||||||
onSubmit={onSubmit}
|
onSubmit={onSubmit}
|
||||||
>
|
>
|
||||||
<Form
|
<Form>
|
||||||
display="grid"
|
<Col maxWidth="600px" gapY="5">
|
||||||
gridTemplateColumns="100%"
|
<Col gapY="1">
|
||||||
gridAutoRows="auto"
|
<Text color="black" fontSize={2} fontWeight="medium">
|
||||||
gridRowGap={5}
|
S3 Storage Setup
|
||||||
>
|
</Text>
|
||||||
<Box color="black" fontSize={1} fontWeight={900}>
|
<Text gray>
|
||||||
S3 Credentials
|
Store credentials for your S3 object storage buckets on your
|
||||||
</Box>
|
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="Endpoint" id="s3endpoint" />
|
||||||
<Input label="Access Key ID" id="s3accessKeyId" />
|
<Input label="Access Key ID" id="s3accessKeyId" />
|
||||||
<Input
|
<Input
|
||||||
@ -77,14 +89,23 @@ export default function S3Form(props: S3FormProps): ReactElement {
|
|||||||
label="Secret Access Key"
|
label="Secret Access Key"
|
||||||
id="s3secretAccessKey"
|
id="s3secretAccessKey"
|
||||||
/>
|
/>
|
||||||
<Button style={{ cursor: 'pointer' }} type="submit">Submit</Button>
|
<Button style={{ cursor: "pointer" }} type="submit">
|
||||||
|
Submit
|
||||||
|
</Button>
|
||||||
|
</Col>
|
||||||
</Form>
|
</Form>
|
||||||
</Formik>
|
</Formik>
|
||||||
</Col>
|
</Col>
|
||||||
<Col>
|
<Col maxWidth="600px" p="5" gapY="4">
|
||||||
<Box color="black" mb={4} fontSize={1} fontWeight={700}>
|
<Col gapY="1">
|
||||||
|
<Text color="black" mb={4} fontSize={2} fontWeight="medium">
|
||||||
S3 Buckets
|
S3 Buckets
|
||||||
</Box>
|
</Text>
|
||||||
|
<Text gray>
|
||||||
|
Your 'active' bucket will be the one used when Landscape uploads a
|
||||||
|
file
|
||||||
|
</Text>
|
||||||
|
</Col>
|
||||||
<BucketList
|
<BucketList
|
||||||
buckets={s3.configuration.buckets}
|
buckets={s3.configuration.buckets}
|
||||||
selected={s3.configuration.currentBucket}
|
selected={s3.configuration.currentBucket}
|
||||||
|
@ -1,41 +1,58 @@
|
|||||||
import React from 'react';
|
import React, { useState } from "react";
|
||||||
import { Box, Button } from '@tlon/indigo-react';
|
import {
|
||||||
|
Box,
|
||||||
|
Text,
|
||||||
|
Button,
|
||||||
|
Col,
|
||||||
|
StatelessCheckboxField,
|
||||||
|
} from "@tlon/indigo-react";
|
||||||
|
|
||||||
import GlobalApi from '../../../../api/global';
|
import GlobalApi from "~/logic/api/global";
|
||||||
|
|
||||||
interface SecuritySettingsProps {
|
interface SecuritySettingsProps {
|
||||||
api: GlobalApi;
|
api: GlobalApi;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function SecuritySettings({ api }: SecuritySettingsProps) {
|
export default function SecuritySettings({ api }: SecuritySettingsProps) {
|
||||||
|
const [allSessions, setAllSessions] = useState(false);
|
||||||
return (
|
return (
|
||||||
<Box display="grid" gridTemplateRows="auto" gridTemplateColumns="1fr" gridRowGap={2}>
|
<Col gapY="5" p="5" pt="4">
|
||||||
<Box color="black" fontSize={1} mb={4} fontWeight={900}>
|
<Col gapY="1">
|
||||||
Security
|
<Text fontSize={2} fontWeight="medium">
|
||||||
</Box>
|
Security Preferences
|
||||||
<Box color="black" fontSize={0} fontWeight={700}>
|
</Text>
|
||||||
|
<Text gray>
|
||||||
|
Manage sessions, login credentials and Landscape access
|
||||||
|
</Text>
|
||||||
|
</Col>
|
||||||
|
<Col gapY="1">
|
||||||
|
<Text color="black">
|
||||||
Log out of this session
|
Log out of this session
|
||||||
</Box>
|
</Text>
|
||||||
<Box fontSize={0} mt={2} color="gray">
|
<Text mb="3" gray>
|
||||||
You will be logged out of your Urbit on this browser.
|
{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">
|
<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
|
Logout
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
</Box>
|
</Col>
|
||||||
<Box color="black" fontSize={0} mt={4} fontWeight={700}>
|
</Col>
|
||||||
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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,35 +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 GlobalApi from "~/logic/api/global";
|
||||||
import { StoreState } from '~/logic/store/type';
|
import { StoreState } from "~/logic/store/type";
|
||||||
import DisplayForm from './lib/DisplayForm';
|
import DisplayForm from "./lib/DisplayForm";
|
||||||
import S3Form from './lib/S3Form';
|
import S3Form from "./lib/S3Form";
|
||||||
import SecuritySettings from './lib/Security';
|
import SecuritySettings from "./lib/Security";
|
||||||
import RemoteContentForm from './lib/RemoteContent';
|
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 function SettingsItem(props: {
|
||||||
|
title: string;
|
||||||
export default function Settings({
|
description: string;
|
||||||
api,
|
to: string;
|
||||||
}: ProfileProps) {
|
}) {
|
||||||
|
const { to, title, description } = props;
|
||||||
return (
|
return (
|
||||||
|
<Link to={`/~settings/${to}`}>
|
||||||
|
<Row alignItems="center" gapX="3">
|
||||||
<Box
|
<Box
|
||||||
backgroundColor="white"
|
borderRadius="2"
|
||||||
display="grid"
|
backgroundColor="blue"
|
||||||
gridTemplateRows="auto"
|
width="64px"
|
||||||
gridTemplateColumns="1fr"
|
height="64px"
|
||||||
gridRowGap={7}
|
|
||||||
p={4}
|
|
||||||
maxWidth="500px"
|
|
||||||
>
|
|
||||||
<DisplayForm
|
|
||||||
api={api}
|
|
||||||
/>
|
/>
|
||||||
<RemoteContentForm api={api} />
|
<Col gapY="2">
|
||||||
<S3Form api={api} />
|
<Text>{title}</Text>
|
||||||
<SecuritySettings api={api} />
|
<Text gray>{description}</Text>
|
||||||
</Box>
|
</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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,47 +1,138 @@
|
|||||||
import React, { ReactElement } from 'react';
|
import React, { ReactNode } from "react";
|
||||||
import { Route } from 'react-router-dom';
|
import { useLocation } from "react-router-dom";
|
||||||
import Helmet from 'react-helmet';
|
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 { NotificationPreferences } from "./components/lib/NotificationPref";
|
||||||
import useLocalState from '~/logic/state/local';
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<Helmet defer={false}>
|
<Helmet defer={false}>
|
||||||
<title>Landscape - Settings</title>
|
<title>Landscape - Settings</title>
|
||||||
</Helmet>
|
</Helmet>
|
||||||
<Route
|
<Skeleton>
|
||||||
path={['/~settings']}
|
<Row height="100%" overflow="hidden">
|
||||||
render={() => {
|
<Col
|
||||||
return (
|
|
||||||
<Box height="100%"
|
|
||||||
width="100%"
|
|
||||||
px={[0, 3]}
|
|
||||||
pb={[0, 3]}
|
|
||||||
borderRadius={1}
|
|
||||||
>
|
|
||||||
<Box
|
|
||||||
height="100%"
|
height="100%"
|
||||||
width="100%"
|
borderRight="1"
|
||||||
display="grid"
|
borderRightColor="washedGray"
|
||||||
gridTemplateColumns={['100%', '400px 1fr']}
|
display={["none", "flex"]}
|
||||||
gridTemplateRows={['48px 1fr', '1fr']}
|
minWidth="250px"
|
||||||
borderRadius={1}
|
maxWidth="350px"
|
||||||
bg="white"
|
|
||||||
border={1}
|
|
||||||
borderColor="washedGray"
|
|
||||||
overflowY="auto"
|
|
||||||
flexGrow
|
|
||||||
>
|
>
|
||||||
<Settings {...props} />
|
<Text
|
||||||
</Box>
|
display="block"
|
||||||
</Box>
|
my="4"
|
||||||
);
|
mx="3"
|
||||||
}}
|
fontSize="2"
|
||||||
|
fontWeight="medium"
|
||||||
|
>
|
||||||
|
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>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -14,12 +14,13 @@ import { hexToUx } from '~/logic/lib/util';
|
|||||||
|
|
||||||
type ColorInputProps = Parameters<typeof Col>[0] & {
|
type ColorInputProps = Parameters<typeof Col>[0] & {
|
||||||
id: string;
|
id: string;
|
||||||
label: string;
|
label?: string;
|
||||||
|
placeholder?: string;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function ColorInput(props: ColorInputProps): ReactElement {
|
export function ColorInput(props: ColorInputProps) {
|
||||||
const { id, label, caption, disabled, ...rest } = props;
|
const { id, placeholder, label, caption, disabled, ...rest } = props;
|
||||||
const [{ value, onBlur }, meta, { setValue }] = useField(id);
|
const [{ value, onBlur }, meta, { setValue }] = useField(id);
|
||||||
|
|
||||||
const hex = value.replace('#', '').replace('0x','').replace('.', '');
|
const hex = value.replace('#', '').replace('0x','').replace('.', '');
|
||||||
@ -54,6 +55,7 @@ export function ColorInput(props: ColorInputProps): ReactElement {
|
|||||||
value={hex}
|
value={hex}
|
||||||
disabled={disabled || false}
|
disabled={disabled || false}
|
||||||
borderRight={0}
|
borderRight={0}
|
||||||
|
placeholder={placeholder}
|
||||||
/>
|
/>
|
||||||
<Box
|
<Box
|
||||||
borderBottomRightRadius={1}
|
borderBottomRightRadius={1}
|
||||||
|
56
pkg/interface/src/views/components/ShuffleFields.tsx
Normal file
56
pkg/interface/src/views/components/ShuffleFields.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
@ -21,12 +21,15 @@ import { uxToHex } from "~/logic/lib/util";
|
|||||||
import { SetStatusBarModal } from './SetStatusBarModal';
|
import { SetStatusBarModal } from './SetStatusBarModal';
|
||||||
import { useTutorialModal } from './useTutorialModal';
|
import { useTutorialModal } from './useTutorialModal';
|
||||||
|
|
||||||
import useLocalState from '~/logic/state/local';
|
|
||||||
import useHarkState from '~/logic/state/hark';
|
import useHarkState from '~/logic/state/hark';
|
||||||
import useInviteState from '~/logic/state/invite';
|
import useInviteState from '~/logic/state/invite';
|
||||||
import { useHistory } from 'react-router-dom';
|
import { useHistory } from 'react-router-dom';
|
||||||
|
import useLocalState, { selectLocalState } from '~/logic/state/local';
|
||||||
|
import useSettingsState, { selectCalmState } from '~/logic/state/settings';
|
||||||
|
|
||||||
|
|
||||||
|
const localSel = selectLocalState(['toggleOmnibox']);
|
||||||
|
|
||||||
const StatusBar = (props) => {
|
const StatusBar = (props) => {
|
||||||
const { ourContact, api, ship } = props;
|
const { ourContact, api, ship } = props;
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
@ -35,10 +38,8 @@ const StatusBar = (props) => {
|
|||||||
const inviteState = useInviteState(state => state.invites);
|
const inviteState = useInviteState(state => state.invites);
|
||||||
const invites = [].concat(...Object.values(inviteState).map(obj => Object.values(obj)));
|
const invites = [].concat(...Object.values(inviteState).map(obj => Object.values(obj)));
|
||||||
const metaKey = (window.navigator.platform.includes('Mac')) ? '⌘' : 'Ctrl+';
|
const metaKey = (window.navigator.platform.includes('Mac')) ? '⌘' : 'Ctrl+';
|
||||||
const { toggleOmnibox, hideAvatars } =
|
const { toggleOmnibox } = useLocalState(localSel);
|
||||||
useLocalState(({ toggleOmnibox, hideAvatars }) =>
|
const { hideAvatars } = useSettingsState(selectCalmState);
|
||||||
({ toggleOmnibox, hideAvatars })
|
|
||||||
);
|
|
||||||
|
|
||||||
const color = !!ourContact ? `#${uxToHex(props.ourContact.color)}` : '#000';
|
const color = !!ourContact ? `#${uxToHex(props.ourContact.color)}` : '#000';
|
||||||
const xPadding = (!hideAvatars && ourContact?.avatar) ? '0' : '2';
|
const xPadding = (!hideAvatars && ourContact?.avatar) ? '0' : '2';
|
||||||
|
@ -9,12 +9,13 @@ import { Associations, Contacts, Groups, Invites } from '@urbit/api';
|
|||||||
import makeIndex from '~/logic/lib/omnibox';
|
import makeIndex from '~/logic/lib/omnibox';
|
||||||
import OmniboxInput from './OmniboxInput';
|
import OmniboxInput from './OmniboxInput';
|
||||||
import OmniboxResult from './OmniboxResult';
|
import OmniboxResult from './OmniboxResult';
|
||||||
import { withLocalState } from '~/logic/state/local';
|
|
||||||
import { deSig } from '~/logic/lib/util';
|
import { deSig } from '~/logic/lib/util';
|
||||||
|
import { withLocalState } from '~/logic/state/local';
|
||||||
|
|
||||||
import defaultApps from '~/logic/lib/default-apps';
|
import defaultApps from '~/logic/lib/default-apps';
|
||||||
import { useOutsideClick } from '~/logic/lib/useOutsideClick';
|
import {useOutsideClick} from '~/logic/lib/useOutsideClick';
|
||||||
import { Portal } from '../Portal';
|
import {Portal} from '../Portal';
|
||||||
|
import useSettingsState, {SettingsState} from '~/logic/state/settings';
|
||||||
import { Tile } from '~/types';
|
import { Tile } from '~/types';
|
||||||
import useContactState from '~/logic/state/contacts';
|
import useContactState from '~/logic/state/contacts';
|
||||||
import useGroupState from '~/logic/state/groups';
|
import useGroupState from '~/logic/state/groups';
|
||||||
@ -24,20 +25,19 @@ import useLaunchState from '~/logic/state/launch';
|
|||||||
import useMetadataState from '~/logic/state/metadata';
|
import useMetadataState from '~/logic/state/metadata';
|
||||||
|
|
||||||
interface OmniboxProps {
|
interface OmniboxProps {
|
||||||
tiles: {
|
|
||||||
[app: string]: Tile;
|
|
||||||
};
|
|
||||||
show: boolean;
|
show: boolean;
|
||||||
toggle: () => void;
|
toggle: () => void;
|
||||||
notifications: number;
|
notifications: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SEARCHED_CATEGORIES = ['ships', 'other', 'commands', 'groups', 'subscriptions', 'apps'];
|
const SEARCHED_CATEGORIES = ['ships', 'other', 'commands', 'groups', 'subscriptions', 'apps'];
|
||||||
|
const settingsSel = (s: SettingsState) => s.leap;
|
||||||
|
|
||||||
export function Omnibox(props: OmniboxProps) {
|
export function Omnibox(props: OmniboxProps) {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const history = useHistory();
|
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 inputRef = useRef<HTMLInputElement | null>(null);
|
||||||
|
|
||||||
const [query, setQuery] = useState('');
|
const [query, setQuery] = useState('');
|
||||||
@ -57,18 +57,30 @@ export function Omnibox(props: OmniboxProps) {
|
|||||||
const groups = useGroupState(state => state.groups);
|
const groups = useGroupState(state => state.groups);
|
||||||
const associations = useMetadataState(state => state.associations);
|
const associations = useMetadataState(state => state.associations);
|
||||||
|
|
||||||
const index = useMemo(() => {
|
const selectedGroup = useMemo(
|
||||||
const selectedGroup = location.pathname.startsWith('/~landscape/ship/')
|
() => location.pathname.startsWith('/~landscape/ship/')
|
||||||
? '/' + location.pathname.split('/').slice(2,5).join('/')
|
? '/' + location.pathname.split('/').slice(2,5).join('/')
|
||||||
: null;
|
: null,
|
||||||
|
[location.pathname]
|
||||||
|
);
|
||||||
|
|
||||||
|
const index = useMemo(() => {
|
||||||
return makeIndex(
|
return makeIndex(
|
||||||
contacts,
|
contacts,
|
||||||
associations,
|
associations,
|
||||||
tiles,
|
tiles,
|
||||||
selectedGroup,
|
selectedGroup,
|
||||||
groups
|
groups,
|
||||||
|
leapConfig,
|
||||||
);
|
);
|
||||||
}, [location.pathname, contacts, associations, groups, tiles]);
|
}, [
|
||||||
|
selectedGroup,
|
||||||
|
leapConfig,
|
||||||
|
contacts,
|
||||||
|
associations,
|
||||||
|
groups,
|
||||||
|
tiles
|
||||||
|
]);
|
||||||
|
|
||||||
const onOutsideClick = useCallback(() => {
|
const onOutsideClick = useCallback(() => {
|
||||||
props.show && props.toggle();
|
props.show && props.toggle();
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component, useEffect } from 'react';
|
||||||
import { Box } from '@tlon/indigo-react';
|
import { Box } from '@tlon/indigo-react';
|
||||||
import { Route, Switch } from 'react-router-dom';
|
import { Route, Switch } from 'react-router-dom';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
@ -12,6 +12,8 @@ import ErrorComponent from '~/views/components/Error';
|
|||||||
import Notifications from '~/views/apps/notifications/notifications';
|
import Notifications from '~/views/apps/notifications/notifications';
|
||||||
import GraphApp from '../../apps/graph/app';
|
import GraphApp from '../../apps/graph/app';
|
||||||
|
|
||||||
|
import { useMigrateSettings } from '~/logic/lib/migrateSettings';
|
||||||
|
|
||||||
|
|
||||||
export const Container = styled(Box)`
|
export const Container = styled(Box)`
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
@ -22,6 +24,14 @@ export const Container = styled(Box)`
|
|||||||
|
|
||||||
|
|
||||||
export const Content = (props) => {
|
export const Content = (props) => {
|
||||||
|
|
||||||
|
const doMigrate = useMigrateSettings();
|
||||||
|
useEffect(() => {
|
||||||
|
setTimeout(() => {
|
||||||
|
doMigrate();
|
||||||
|
}, 10000);
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
<Switch>
|
<Switch>
|
||||||
|
@ -31,6 +31,7 @@ import GlobalApi from '~/logic/api/global';
|
|||||||
import { StatelessAsyncAction } from '~/views/components/StatelessAsyncAction';
|
import { StatelessAsyncAction } from '~/views/components/StatelessAsyncAction';
|
||||||
import useLocalState from '~/logic/state/local';
|
import useLocalState from '~/logic/state/local';
|
||||||
import useContactState from '~/logic/state/contacts';
|
import useContactState from '~/logic/state/contacts';
|
||||||
|
import useSettingsState, { selectCalmState } from '~/logic/state/settings';
|
||||||
|
|
||||||
const TruncText = styled(Text)`
|
const TruncText = styled(Text)`
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
@ -80,13 +81,14 @@ function getParticipants(cs: Contacts, group: Group) {
|
|||||||
|
|
||||||
const emptyContact = (patp: string, pending: boolean): Participant => ({
|
const emptyContact = (patp: string, pending: boolean): Participant => ({
|
||||||
nickname: '',
|
nickname: '',
|
||||||
email: '',
|
bio: '',
|
||||||
phone: '',
|
status: '',
|
||||||
color: '',
|
color: '',
|
||||||
avatar: null,
|
avatar: null,
|
||||||
notes: '',
|
cover: null,
|
||||||
website: '',
|
groups: [],
|
||||||
patp,
|
patp,
|
||||||
|
'last-updated': 0,
|
||||||
pending
|
pending
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -257,9 +259,7 @@ function Participant(props: {
|
|||||||
}) {
|
}) {
|
||||||
const { contact, association, group, api } = props;
|
const { contact, association, group, api } = props;
|
||||||
const { title } = association.metadata;
|
const { title } = association.metadata;
|
||||||
const { hideAvatars, hideNicknames } = useLocalState(
|
const { hideAvatars, hideNicknames } = useSettingsState(selectCalmState);
|
||||||
({ hideAvatars, hideNicknames }) => ({ hideAvatars, hideNicknames })
|
|
||||||
);
|
|
||||||
|
|
||||||
const color = uxToHex(contact.color);
|
const color = uxToHex(contact.color);
|
||||||
const isInvite = 'invite' in group.policy;
|
const isInvite = 'invite' in group.policy;
|
||||||
|
@ -8,12 +8,13 @@ import { HoverBoxLink } from '~/views/components/HoverBox';
|
|||||||
import { Sigil } from '~/logic/lib/sigil';
|
import { Sigil } from '~/logic/lib/sigil';
|
||||||
import { getModuleIcon, getItemTitle, uxToHex } from '~/logic/lib/util';
|
import { getModuleIcon, getItemTitle, uxToHex } from '~/logic/lib/util';
|
||||||
import { useTutorialModal } from '~/views/components/useTutorialModal';
|
import { useTutorialModal } from '~/views/components/useTutorialModal';
|
||||||
import useLocalState from '~/logic/state/local';
|
|
||||||
import { TUTORIAL_HOST, TUTORIAL_GROUP } from '~/logic/lib/tutorialModal';
|
import { TUTORIAL_HOST, TUTORIAL_GROUP } from '~/logic/lib/tutorialModal';
|
||||||
import { SidebarAppConfigs, SidebarItemStatus } from './types';
|
import { SidebarAppConfigs, SidebarItemStatus } from './types';
|
||||||
import { Workspace } from '~/types/workspace';
|
import { Workspace } from '~/types/workspace';
|
||||||
import useContactState from '~/logic/state/contacts';
|
import useContactState from '~/logic/state/contacts';
|
||||||
import useGroupState from '~/logic/state/groups';
|
import useGroupState from '~/logic/state/groups';
|
||||||
|
import useSettingsState, { selectCalmState } from '~/logic/state/settings';
|
||||||
|
|
||||||
|
|
||||||
function SidebarItemIndicator(props: { status?: SidebarItemStatus }) {
|
function SidebarItemIndicator(props: { status?: SidebarItemStatus }) {
|
||||||
switch (props.status) {
|
switch (props.status) {
|
||||||
@ -57,9 +58,8 @@ export function SidebarItem(props: {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const DM = (isUnmanaged && props.workspace?.type === 'messages');
|
const DM = (isUnmanaged && props.workspace?.type === 'messages');
|
||||||
const { hideAvatars, hideNicknames } = useLocalState(({ hideAvatars, hideNicknames }) => ({
|
const { hideAvatars, hideNicknames } = useSettingsState(selectCalmState);
|
||||||
hideAvatars, hideNicknames
|
|
||||||
}));
|
|
||||||
const itemStatus = app.getStatus(path);
|
const itemStatus = app.getStatus(path);
|
||||||
const hasUnread = itemStatus === 'unread' || itemStatus === 'mention';
|
const hasUnread = itemStatus === 'unread' || itemStatus === 'mention';
|
||||||
|
|
||||||
|
@ -34,7 +34,7 @@ export const SidebarItem = ({
|
|||||||
justifyContent="space-between"
|
justifyContent="space-between"
|
||||||
{...rest}
|
{...rest}
|
||||||
>
|
>
|
||||||
<Row>
|
<Row alignItems="center">
|
||||||
<Icon color={color} icon={icon as any} mr="2" />
|
<Icon color={color} icon={icon as any} mr="2" />
|
||||||
<Text color={color}>{text}</Text>
|
<Text color={color}>{text}</Text>
|
||||||
</Row>
|
</Row>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
export type Key = string;
|
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 Bucket = Map<string, Value>;
|
||||||
export type Settings = Map<string, Bucket>;
|
export type Settings = Map<string, Bucket>;
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user