diff --git a/pkg/interface/src/logic/lib/gcpManager.ts b/pkg/interface/src/logic/lib/gcpManager.ts index 94bfa9461..4831ffc5c 100644 --- a/pkg/interface/src/logic/lib/gcpManager.ts +++ b/pkg/interface/src/logic/lib/gcpManager.ts @@ -76,7 +76,8 @@ class GcpManager { if (this.isConfigured()) { this.refreshLoop(); } else { - this.refreshAfter(10_000); + console.log('GcpManager: GCP storage not configured; stopping.'); + this.stop(); } }) .catch((reason) => { diff --git a/pkg/interface/src/logic/store/store.ts b/pkg/interface/src/logic/store/store.ts index 5aecf49b2..3f77beb9c 100644 --- a/pkg/interface/src/logic/store/store.ts +++ b/pkg/interface/src/logic/store/store.ts @@ -20,6 +20,7 @@ import GcpReducer from '../reducers/gcp-reducer'; import { OrderedMap } from '../lib/OrderedMap'; import { BigIntOrderedMap } from '../lib/BigIntOrderedMap'; import { GroupViewReducer } from '../reducers/group-view'; +import { unstable_batchedUpdates } from 'react-dom'; export default class GlobalStore extends BaseStore { inviteReducer = new InviteReducer(); @@ -50,21 +51,23 @@ export default class GlobalStore extends BaseStore { } reduce(data: Cage, state: StoreState) { - // debug shim - const tag = Object.keys(data)[0]; - const oldActions = this.pastActions[tag] || []; - this.pastActions[tag] = [data[tag], ...oldActions.slice(0,14)]; - this.inviteReducer.reduce(data); - this.metadataReducer.reduce(data); - this.s3Reducer.reduce(data); - this.groupReducer.reduce(data); - GroupViewReducer(data); - this.launchReducer.reduce(data); - this.connReducer.reduce(data, this.state); - GraphReducer(data); - HarkReducer(data); - ContactReducer(data); - this.settingsReducer.reduce(data); - this.gcpReducer.reduce(data); + unstable_batchedUpdates(() => { + // debug shim + const tag = Object.keys(data)[0]; + const oldActions = this.pastActions[tag] || []; + this.pastActions[tag] = [data[tag], ...oldActions.slice(0, 14)]; + this.inviteReducer.reduce(data); + this.metadataReducer.reduce(data); + this.s3Reducer.reduce(data); + this.groupReducer.reduce(data); + GroupViewReducer(data); + this.launchReducer.reduce(data); + this.connReducer.reduce(data, this.state); + GraphReducer(data); + HarkReducer(data); + ContactReducer(data); + this.settingsReducer.reduce(data); + this.gcpReducer.reduce(data); + }); } } diff --git a/pkg/interface/src/views/apps/chat/components/chat-editor.js b/pkg/interface/src/views/apps/chat/components/chat-editor.js index 03910afc2..1d0563e58 100644 --- a/pkg/interface/src/views/apps/chat/components/chat-editor.js +++ b/pkg/interface/src/views/apps/chat/components/chat-editor.js @@ -199,6 +199,7 @@ export default class ChatEditor extends Component { width='calc(100% - 88px)' className={inCodeMode ? 'chat code' : 'chat'} color="black" + overflow='scroll' > {MOBILE_BROWSER_REGEX.test(navigator.userAgent) ? { const { unreadCount, unreadMsg, dismissUnread, onClick } = props; - if (!unreadMsg || (unreadCount === 0)) { + if (!unreadMsg || unreadCount === 0) { return null; } const stamp = moment.unix(unreadMsg.post['time-sent'] / 1000); - let datestamp = moment.unix(unreadMsg.post['time-sent'] / 1000).format('YYYY.M.D'); - const timestamp = moment.unix(unreadMsg.post['time-sent'] / 1000).format('HH:mm'); + let datestamp = moment + .unix(unreadMsg.post['time-sent'] / 1000) + .format('YYYY.M.D'); + const timestamp = moment + .unix(unreadMsg.post['time-sent'] / 1000) + .format('HH:mm'); if (datestamp === moment().format('YYYY.M.D')) { datestamp = null; } return ( - - - - {unreadCount} new message{unreadCount > 1 ? 's' : ''} since{' '} - - - - Mark as Read - - +
+ + + + {unreadCount} new message{unreadCount > 1 ? 's' : ''} since{' '} + + + + + +
); -} +}; diff --git a/pkg/interface/src/views/apps/launch/components/Groups.tsx b/pkg/interface/src/views/apps/launch/components/Groups.tsx index a474d2f8e..e93016e35 100644 --- a/pkg/interface/src/views/apps/launch/components/Groups.tsx +++ b/pkg/interface/src/views/apps/launch/components/Groups.tsx @@ -93,7 +93,10 @@ function Group(props: GroupProps) { ); const { hideUnreads } = useSettingsState(selectCalmState); const joined = useSettingsState(selectJoined); - const days = Math.floor(moment.duration(moment(joined).add(14, 'days').diff(moment())).as('days')); + const days = Math.max(0, Math.floor(moment.duration(moment(joined) + .add(14, 'days') + .diff(moment())) + .as('days'))) || 0; return ( diff --git a/pkg/interface/src/views/apps/profile/components/Profile.tsx b/pkg/interface/src/views/apps/profile/components/Profile.tsx index ab0801d85..fb7ab4835 100644 --- a/pkg/interface/src/views/apps/profile/components/Profile.tsx +++ b/pkg/interface/src/views/apps/profile/components/Profile.tsx @@ -112,6 +112,7 @@ export function ProfileStatus(props: any): ReactElement { display='inline-block' verticalAlign='middle' color='gray' + title={contact?.status ?? ''} > {contact?.status ?? ''} diff --git a/pkg/interface/src/views/apps/publish/css/custom.css b/pkg/interface/src/views/apps/publish/css/custom.css index 920c88947..e3768772c 100644 --- a/pkg/interface/src/views/apps/publish/css/custom.css +++ b/pkg/interface/src/views/apps/publish/css/custom.css @@ -142,6 +142,10 @@ margin-bottom: 16px; } +.md ul ul { + margin-bottom: 0px; +} + .md h2, .md h3, .md h4, .md h5, .md p, .md a, .md ul { font-weight: 400; } diff --git a/pkg/interface/src/views/apps/settings/components/lib/CalmPref.tsx b/pkg/interface/src/views/apps/settings/components/lib/CalmPref.tsx index 16530b573..2047f73b2 100644 --- a/pkg/interface/src/views/apps/settings/components/lib/CalmPref.tsx +++ b/pkg/interface/src/views/apps/settings/components/lib/CalmPref.tsx @@ -54,10 +54,10 @@ export function CalmPrefs(props: { hideUnreads, hideGroups, hideUtilities, - imageShown, - videoShown, - oembedShown, - audioShown, + imageShown: !imageShown, + videoShown: !videoShown, + oembedShown: !oembedShown, + audioShown: !audioShown }; const onSubmit = useCallback(async (v: FormSchema, actions: FormikHelpers) => { @@ -67,10 +67,10 @@ export function CalmPrefs(props: { api.settings.putEntry('calm', 'hideUnreads', v.hideUnreads), api.settings.putEntry('calm', 'hideGroups', v.hideGroups), api.settings.putEntry('calm', 'hideUtilities', v.hideUtilities), - 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), + 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]); @@ -115,24 +115,24 @@ export function CalmPrefs(props: { id="hideNicknames" caption="Do not show user-set nicknames" /> - Remote Content + Remote content diff --git a/pkg/interface/src/views/apps/settings/components/lib/RemoteContent.tsx b/pkg/interface/src/views/apps/settings/components/lib/RemoteContent.tsx deleted file mode 100644 index 1ab87e54b..000000000 --- a/pkg/interface/src/views/apps/settings/components/lib/RemoteContent.tsx +++ /dev/null @@ -1,85 +0,0 @@ -import React from 'react'; -import { - Box, - Button, - ManagedCheckboxField as Checkbox -} from '@tlon/indigo-react'; -import { Formik, Form } from 'formik'; -import * as Yup from 'yup'; - -import GlobalApi from '~/logic/api/global'; -import useSettingsState, {selectSettingsState} from '~/logic/state/settings'; - -const formSchema = Yup.object().shape({ - imageShown: Yup.boolean(), - audioShown: Yup.boolean(), - videoShown: Yup.boolean(), - oembedShown: Yup.boolean() -}); - -interface FormSchema { - imageShown: boolean; - audioShown: boolean; - videoShown: boolean; - oembedShown: boolean; -} - -interface RemoteContentFormProps { - api: GlobalApi; -} -const selState = selectSettingsState(['remoteContentPolicy', 'set']); - -export default function RemoteContentForm(props: RemoteContentFormProps) { - const { api } = props; - const { remoteContentPolicy, set: setRemoteContentPolicy} = useSettingsState(selState); - const imageShown = remoteContentPolicy.imageShown; - const audioShown = remoteContentPolicy.audioShown; - const videoShown = remoteContentPolicy.videoShown; - const oembedShown = remoteContentPolicy.oembedShown; - return ( - { - setRemoteContentPolicy((state) => { - Object.assign(state.remoteContentPolicy, values); - }); - actions.setSubmitting(false); - }} - > - {props => ( -
- - - Remote Content - - - - - - - -
- )} -
- ); -} - diff --git a/pkg/interface/src/views/apps/settings/components/lib/S3Form.tsx b/pkg/interface/src/views/apps/settings/components/lib/S3Form.tsx index 24f10b7a9..005aa563f 100644 --- a/pkg/interface/src/views/apps/settings/components/lib/S3Form.tsx +++ b/pkg/interface/src/views/apps/settings/components/lib/S3Form.tsx @@ -1,5 +1,5 @@ import React, { ReactElement, useCallback } from 'react'; -import { Formik } from 'formik'; +import { Formik, FormikHelpers } from 'formik'; import { ManagedTextInputField as Input, @@ -10,6 +10,7 @@ import { Col, Anchor } from '@tlon/indigo-react'; +import { AsyncButton } from "~/views/components/AsyncButton"; import GlobalApi from '~/logic/api/global'; import { BucketList } from './BucketList'; @@ -35,19 +36,19 @@ export default function S3Form(props: S3FormProps): ReactElement { const { api } = props; const s3 = useStorageState((state) => state.s3); - const onSubmit = useCallback( - (values: FormSchema) => { + const onSubmit = useCallback(async (values: FormSchema, actions: FormikHelpers) => { if (values.s3secretAccessKey !== s3.credentials?.secretAccessKey) { - api.s3.setSecretAccessKey(values.s3secretAccessKey); + await api.s3.setSecretAccessKey(values.s3secretAccessKey); } if (values.s3endpoint !== s3.credentials?.endpoint) { - api.s3.setEndpoint(values.s3endpoint); + await api.s3.setEndpoint(values.s3endpoint); } if (values.s3accessKeyId !== s3.credentials?.accessKeyId) { - api.s3.setAccessKeyId(values.s3accessKeyId); + await api.s3.setAccessKeyId(values.s3accessKeyId); } + actions.setStatus({ success: null }); }, [api, s3] ); @@ -95,9 +96,9 @@ export default function S3Form(props: S3FormProps): ReactElement { label='Secret Access Key' id='s3secretAccessKey' /> - + diff --git a/pkg/interface/src/views/apps/settings/components/settings.tsx b/pkg/interface/src/views/apps/settings/components/settings.tsx index 9deeec6eb..9c49fd414 100644 --- a/pkg/interface/src/views/apps/settings/components/settings.tsx +++ b/pkg/interface/src/views/apps/settings/components/settings.tsx @@ -7,7 +7,6 @@ import { StoreState } from "~/logic/store/type"; import DisplayForm from "./lib/DisplayForm"; import S3Form from "./lib/S3Form"; import SecuritySettings from "./lib/Security"; -import RemoteContentForm from "./lib/RemoteContent"; import { NotificationPreferences } from "./lib/NotificationPref"; import { CalmPrefs } from "./lib/CalmPref"; import { Link } from "react-router-dom"; diff --git a/pkg/interface/src/views/components/Invite/index.tsx b/pkg/interface/src/views/components/Invite/index.tsx index 2aee9f3c6..073abc2ae 100644 --- a/pkg/interface/src/views/components/Invite/index.tsx +++ b/pkg/interface/src/views/components/Invite/index.tsx @@ -11,7 +11,7 @@ import { import { Invite } from '@urbit/api/invite'; import { Text, Icon, Row } from '@tlon/indigo-react'; -import { cite } from '~/logic/lib/util'; +import { cite, useShowNickname } from '~/logic/lib/util'; import GlobalApi from '~/logic/api/global'; import { resourceFromPath } from '~/logic/lib/group'; import { GroupInvite } from './Group'; @@ -19,6 +19,7 @@ import { InviteSkeleton } from './InviteSkeleton'; import { JoinSkeleton } from './JoinSkeleton'; import { useWaitForProps } from '~/logic/lib/useWaitForProps'; import useGroupState from '~/logic/state/group'; +import useContactState from '~/logic/state/contact'; import useMetadataState from '~/logic/state/metadata'; import useGraphState from '~/logic/state/graph'; @@ -38,6 +39,9 @@ export function InviteItem(props: InviteItemProps) { const groups = useGroupState(state => state.groups); const graphKeys = useGraphState(s => s.graphKeys); const associations = useMetadataState(state => state.associations); + const contacts = useContactState(state => state.contacts); + const contact = contacts?.[`~${invite?.ship}`] ?? {}; + const showNickname = useShowNickname(contact); const waiter = useWaitForProps( { associations, groups, pendingJoin, graphKeys: Array.from(graphKeys) }, 50000 @@ -119,8 +123,10 @@ export function InviteItem(props: InviteItemProps) { > - - {cite(`~${invite!.ship}`)} + + {showNickname ? contact?.nickname : cite(`~${invite!.ship}`)} invited you to a DM @@ -145,8 +151,10 @@ export function InviteItem(props: InviteItemProps) { > - - {cite(`~${invite!.ship}`)} + + {showNickname ? contact?.nickname : cite(`~${invite!.ship}`)} invited you to ~{invite.resource.ship}/{invite.resource.name} diff --git a/pkg/interface/src/views/components/ProfileOverlay.tsx b/pkg/interface/src/views/components/ProfileOverlay.tsx index 6ff25d4ef..6d653b8ae 100644 --- a/pkg/interface/src/views/components/ProfileOverlay.tsx +++ b/pkg/interface/src/views/components/ProfileOverlay.tsx @@ -11,7 +11,8 @@ import { Text, BaseImage, ColProps, - Icon + Icon, + Center } from '@tlon/indigo-react'; import RichText from './RichText'; import { ProfileStatus } from './ProfileStatus'; @@ -44,16 +45,19 @@ const ProfileOverlay = (props: ProfileOverlayProps) => { onDismiss, ...rest } = props; - const hideAvatars = useSettingsState(state => state.calm.hideAvatars); - const hideNicknames = useSettingsState(state => state.calm.hideNicknames); + const hideAvatars = useSettingsState((state) => state.calm.hideAvatars); + const hideNicknames = useSettingsState((state) => state.calm.hideNicknames); const popoverRef = useRef(null); - const onDocumentClick = useCallback((event) => { - if (!popoverRef.current || popoverRef?.current?.contains(event.target)) { - return; - } - onDismiss(); - }, [onDismiss, popoverRef]); + const onDocumentClick = useCallback( + (event) => { + if (!popoverRef.current || popoverRef?.current?.contains(event.target)) { + return; + } + onDismiss(); + }, + [onDismiss, popoverRef] + ); useEffect(() => { document.addEventListener('mousedown', onDocumentClick); @@ -62,123 +66,124 @@ const ProfileOverlay = (props: ProfileOverlayProps) => { return () => { document.removeEventListener('mousedown', onDocumentClick); document.removeEventListener('touchstart', onDocumentClick); - } + }; }, [onDocumentClick]); let top, bottom; - if (topSpace < OVERLAY_HEIGHT / 2) { - top = '0px'; - } - if (bottomSpace < OVERLAY_HEIGHT / 2) { - bottom = '0px'; - } - if (!(top || bottom)) { - bottom = `-${Math.round(OVERLAY_HEIGHT / 2)}px`; - } - const containerStyle = { top, bottom, left: '100%' }; + if (topSpace < OVERLAY_HEIGHT / 2) { + top = '0px'; + } + if (bottomSpace < OVERLAY_HEIGHT / 2) { + bottom = '0px'; + } + if (!(top || bottom)) { + bottom = `-${Math.round(OVERLAY_HEIGHT / 2)}px`; + } + const containerStyle = { top, bottom, left: '100%' }; - const isOwn = window.ship === ship; + const isOwn = window.ship === ship; - const img = - contact?.avatar && !hideAvatars ? ( - - ) : ( - - ); - const showNickname = useShowNickname(contact, hideNicknames); - - return ( - - - {!isOwn && ( - history.push(`/~landscape/dm/${ship}`)} - /> - )} - - history.push(`/~profile/~${ship}`)} - overflow='hidden' - borderRadius={2} - > - {img} - - - - - {showNickname ? contact?.nickname : cite(ship)} - - - {isOwn ? ( - - ) : ( - - {contact?.status ? contact.status : ''} - - )} - - + /> + ) : ( + +
+ +
+
); + const showNickname = useShowNickname(contact, hideNicknames); + + return ( + + + {!isOwn && ( + history.push(`/~landscape/dm/${ship}`)} + /> + )} + + history.push(`/~profile/~${ship}`)} + overflow='hidden' + borderRadius={2} + > + {img} + + + + + {showNickname ? contact?.nickname : cite(ship)} + + + {isOwn ? ( + + ) : ( + + {contact?.status ?? ''} + + )} + + + ); }; -export default ProfileOverlay; \ No newline at end of file +export default ProfileOverlay; diff --git a/pkg/interface/src/views/components/Timestamp.tsx b/pkg/interface/src/views/components/Timestamp.tsx index 8ae91336d..a73ce72eb 100644 --- a/pkg/interface/src/views/components/Timestamp.tsx +++ b/pkg/interface/src/views/components/Timestamp.tsx @@ -11,20 +11,24 @@ export type TimestampProps = BoxProps & { stamp: MomentType; date?: boolean; time?: boolean; -} +}; -const Timestamp = (props: TimestampProps): ReactElement | null=> { +const Timestamp = (props: TimestampProps): ReactElement | null => { const { stamp, date, time, color, fontSize, ...rest } = { - time: true, color: 'gray', fontSize: 0, ...props + time: true, + color: 'gray', + fontSize: 0, + ...props }; if (!stamp) return null; - const { hovering, bind } = date === true - ? { hovering: true, bind: {} } - : useHovering(); + const { hovering, bind } = + date === true ? { hovering: true, bind: {} } : useHovering(); let datestamp = stamp.format(DateFormat); if (stamp.format(DateFormat) === moment().format(DateFormat)) { datestamp = 'Today'; - } else if (stamp.format(DateFormat) === moment().subtract(1, 'day').format(DateFormat)) { + } else if ( + stamp.format(DateFormat) === moment().subtract(1, 'day').format(DateFormat) + ) { datestamp = 'Yesterday'; } const timestamp = stamp.format(TimeFormat); @@ -33,22 +37,28 @@ const Timestamp = (props: TimestampProps): ReactElement | null=> { {...bind} display='flex' flex='row' - flexWrap="nowrap" + flexWrap='nowrap' {...rest} title={stamp.format(DateFormat + ' ' + TimeFormat)} > - {time && {timestamp}} - {date !== false && - {datestamp} - } + {time && ( + + {timestamp} + + )} + {date !== false && ( + + {time ? '\u00A0' : ''} + {datestamp} + + )} - ) -} + ); +}; -export default Timestamp; \ No newline at end of file +export default Timestamp; diff --git a/pkg/interface/src/views/components/leap/Omnibox.tsx b/pkg/interface/src/views/components/leap/Omnibox.tsx index 232d1915e..6884a1692 100644 --- a/pkg/interface/src/views/components/leap/Omnibox.tsx +++ b/pkg/interface/src/views/components/leap/Omnibox.tsx @@ -30,7 +30,7 @@ interface OmniboxProps { notifications: number; } -const SEARCHED_CATEGORIES = ['ships', 'other', 'commands', 'groups', 'subscriptions', 'apps']; +const SEARCHED_CATEGORIES = ['commands', 'ships', 'other', 'groups', 'subscriptions', 'apps']; const settingsSel = (s: SettingsState) => s.leap; export function Omnibox(props: OmniboxProps) { @@ -251,6 +251,15 @@ export function Omnibox(props: OmniboxProps) { setQuery(event.target.value); }, []); + // Sort Omnibox results alphabetically + const sortResults = (a: Record<'title', string>, b: Record<'title', string>) => { + // Do not sort unless searching (preserves order of menu actions) + if (query === '') { return 0 }; + if (a.title < b.title) { return -1 }; + if (a.title > b.title) { return 1 }; + return 0; + } + const renderResults = useCallback(() => { return {categoryTitle} - {categoryResults.map((result, i2) => ( - navigate(result.app, result.link)} - selected={sel} - /> + {categoryResults + .sort(sortResults) + .map((result, i2) => ( + navigate(result.app, result.link)} + selected={sel} + /> ))} ); diff --git a/pkg/interface/src/views/components/leap/OmniboxResult.js b/pkg/interface/src/views/components/leap/OmniboxResult.js index 6db19f29c..c44114688 100644 --- a/pkg/interface/src/views/components/leap/OmniboxResult.js +++ b/pkg/interface/src/views/components/leap/OmniboxResult.js @@ -64,7 +64,7 @@ export class OmniboxResult extends Component { graphic = ; } else if (icon === 'tutorial') { graphic = ; - } + } else { graphic = ; } @@ -102,6 +102,12 @@ export class OmniboxResult extends Component { {text.startsWith("~") ? cite(text) : text}