diff options
author | João Ferreiro <ferreiro@pinkroom.dev> | 2022-11-28 16:56:05 +0000 |
---|---|---|
committer | João Ferreiro <ferreiro@pinkroom.dev> | 2022-11-28 16:56:05 +0000 |
commit | c5f3200d6b561af94ec98259e731d9e090719df0 (patch) | |
tree | 1847500a3041ed9d1642e12c6f01e3581b8aafb6 | |
parent | 5ea750599d08229d4b5b10d0e724ca14c73735f5 (diff) | |
parent | b9c9895c45158b3db52e07114ad4305d85e803ea (diff) | |
download | voidsky-c5f3200d6b561af94ec98259e731d9e090719df0.tar.zst |
Merge branch 'main' into upload-image
27 files changed, 425 insertions, 429 deletions
diff --git a/__tests__/string-utils.ts b/__tests__/string-utils.ts index c677b44d3..fc7a8f272 100644 --- a/__tests__/string-utils.ts +++ b/__tests__/string-utils.ts @@ -31,6 +31,11 @@ describe('extractEntities', () => { 'start middle end.com/foo/bar?baz=bux#hash', 'newline1.com\nnewline2.com', 'not.. a..url ..here', + 'e.g.', + 'something-cool.jpg', + 'website.com.jpg', + 'e.g./foo', + 'website.com.jpg/foo', ] interface Output { type: string @@ -80,6 +85,11 @@ describe('extractEntities', () => { {type: 'link', value: 'newline2.com', noScheme: true}, ], [], + [], + [], + [], + [], + [], ] it('correctly handles a set of text inputs', () => { for (let i = 0; i < inputs.length; i++) { @@ -145,6 +155,12 @@ describe('detectLinkables', () => { 'start middle end.com/foo/bar?baz=bux#hash', 'newline1.com\nnewline2.com', 'not.. a..url ..here', + 'e.g.', + 'e.g. real.com fake.notreal', + 'something-cool.jpg', + 'website.com.jpg', + 'e.g./foo', + 'website.com.jpg/foo', ] const outputs = [ ['no linkable'], @@ -171,6 +187,12 @@ describe('detectLinkables', () => { ['start middle ', {link: 'end.com/foo/bar?baz=bux#hash'}], [{link: 'newline1.com'}, '\n', {link: 'newline2.com'}], ['not.. a..url ..here'], + ['e.g.'], + ['e.g. ', {link: 'real.com'}, ' fake.notreal'], + ['something-cool.jpg'], + ['website.com.jpg'], + ['e.g./foo'], + ['website.com.jpg/foo'], ] it('correctly handles a set of text inputs', () => { for (let i = 0; i < inputs.length; i++) { diff --git a/package.json b/package.json index 35e4615a9..13af3997a 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,8 @@ "react-native-svg": "^12.4.0", "react-native-tab-view": "^3.3.0", "react-native-url-polyfill": "^1.3.0", - "react-native-web": "^0.17.7" + "react-native-web": "^0.17.7", + "tlds": "^1.234.0" }, "devDependencies": { "@babel/core": "^7.12.9", diff --git a/src/App.web.tsx b/src/App.web.tsx index 06da5e4e3..cc6f3815b 100644 --- a/src/App.web.tsx +++ b/src/App.web.tsx @@ -2,7 +2,7 @@ import React, {useState, useEffect} from 'react' import * as view from './view/index' import {RootStoreModel, setupState, RootStoreProvider} from './state' import {DesktopWebShell} from './view/shell/desktop-web' -import Toast from './view/com/util/Toast' +import Toast from 'react-native-root-toast' function App() { const [rootStore, setRootStore] = useState<RootStoreModel | undefined>( diff --git a/src/lib/strings.ts b/src/lib/strings.ts index 032eec566..fb9d15b29 100644 --- a/src/lib/strings.ts +++ b/src/lib/strings.ts @@ -1,6 +1,7 @@ import {AtUri} from '../third-party/uri' import {Entity} from '../third-party/api/src/client/types/app/bsky/feed/post' import {PROD_SERVICE} from '../state' +import TLDs from 'tlds' export const MAX_DISPLAY_NAME = 64 export const MAX_DESCRIPTION = 256 @@ -57,6 +58,14 @@ export function ago(date: number | string | Date): string { } } +export function isValidDomain(str: string): boolean { + return !!TLDs.find(tld => { + let i = str.lastIndexOf(tld) + if (i === -1) return false + return str.charAt(i - 1) === '.' && i === str.length - tld.length + }) +} + export function extractEntities( text: string, knownHandles?: Set<string>, @@ -85,10 +94,14 @@ export function extractEntities( { // links const re = - /(^|\s)((https?:\/\/[\S]+)|([a-z][a-z0-9]*(\.[a-z0-9]+)+[\S]*))(\b)/dg + /(^|\s)((https?:\/\/[\S]+)|((?<domain>[a-z][a-z0-9]*(\.[a-z0-9]+)+)[\S]*))(\b)/dg while ((match = re.exec(text))) { let value = match[2] if (!value.startsWith('http')) { + const domain = match.groups?.domain + if (!domain || !isValidDomain(domain)) { + continue + } value = `https://${value}` } ents.push({ @@ -110,7 +123,7 @@ interface DetectedLink { type DetectedLinkable = string | DetectedLink export function detectLinkables(text: string): DetectedLinkable[] { const re = - /((^|\s)@[a-z0-9\.-]*)|((^|\s)https?:\/\/[\S]+)|((^|\s)[a-z][a-z0-9]*(\.[a-z0-9]+)+[\S]*)/gi + /((^|\s)@[a-z0-9\.-]*)|((^|\s)https?:\/\/[\S]+)|((^|\s)(?<domain>[a-z][a-z0-9]*(\.[a-z0-9]+)+)[\S]*)/gi const segments = [] let match let start = 0 @@ -118,6 +131,10 @@ export function detectLinkables(text: string): DetectedLinkable[] { let matchIndex = match.index let matchValue = match[0] + if (match.groups?.domain && !isValidDomain(match.groups?.domain)) { + continue + } + if (/\s/.test(matchValue)) { // HACK // skip the starting space diff --git a/src/state/models/feed-view.ts b/src/state/models/feed-view.ts index 92c394dfa..33db426a4 100644 --- a/src/state/models/feed-view.ts +++ b/src/state/models/feed-view.ts @@ -7,6 +7,8 @@ import * as apilib from '../lib/api' import {cleanError} from '../../lib/strings' import {isObj, hasProp} from '../lib/type-guards' +const PAGE_SIZE = 30 + type FeedItem = GetTimeline.FeedItem | GetAuthorFeed.FeedItem type FeedItemWithThreadMeta = FeedItem & { _isThreadParent?: boolean @@ -166,6 +168,7 @@ export class FeedModel { params: GetTimeline.QueryParams | GetAuthorFeed.QueryParams hasMore = true loadMoreCursor: string | undefined + pollCursor: string | undefined _loadPromise: Promise<void> | undefined _loadMorePromise: Promise<void> | undefined _loadLatestPromise: Promise<void> | undefined @@ -300,7 +303,7 @@ export class FeedModel { const res = await this._getFeed({limit: 1}) this.setHasNewLatest( res.data.feed[0] && - (this.feed.length === 0 || res.data.feed[0].uri !== this.feed[0]?.uri), + (this.feed.length === 0 || res.data.feed[0].uri !== this.pollCursor), ) } @@ -341,7 +344,7 @@ export class FeedModel { private async _initialLoad(isRefreshing = false) { this._xLoading(isRefreshing) try { - const res = await this._getFeed() + const res = await this._getFeed({limit: PAGE_SIZE}) this._replaceAll(res) this._xIdle() } catch (e: any) { @@ -352,7 +355,7 @@ export class FeedModel { private async _loadLatest() { this._xLoading() try { - const res = await this._getFeed() + const res = await this._getFeed({limit: PAGE_SIZE}) this._prependAll(res) this._xIdle() } catch (e: any) { @@ -368,6 +371,7 @@ export class FeedModel { try { const res = await this._getFeed({ before: this.loadMoreCursor, + limit: PAGE_SIZE, }) this._appendAll(res) this._xIdle() @@ -402,6 +406,7 @@ export class FeedModel { private _replaceAll(res: GetTimeline.Response | GetAuthorFeed.Response) { this.feed.length = 0 + this.pollCursor = res.data.feed[0]?.uri this._appendAll(res) } @@ -434,6 +439,7 @@ export class FeedModel { } private _prependAll(res: GetTimeline.Response | GetAuthorFeed.Response) { + this.pollCursor = res.data.feed[0]?.uri let counter = this.feed.length const toPrepend = [] for (const item of res.data.feed) { @@ -493,8 +499,7 @@ function preprocessFeed( for (let i = feed.length - 1; i >= 0; i--) { const item = feed[i] as FeedItemWithThreadMeta - // dont dedup the first item so that polling works properly - if (dedup && i !== 0) { + if (dedup) { if (reorg.find(item2 => item2.uri === item.uri)) { continue } diff --git a/src/state/models/notifications-view.ts b/src/state/models/notifications-view.ts index 09189cfbb..80e5c80c6 100644 --- a/src/state/models/notifications-view.ts +++ b/src/state/models/notifications-view.ts @@ -7,7 +7,7 @@ import {APP_BSKY_GRAPH} from '../../third-party/api' import {cleanError} from '../../lib/strings' const UNGROUPABLE_REASONS = ['trend', 'assertion'] - +const PAGE_SIZE = 30 const MS_60MIN = 1e3 * 60 * 60 export interface GroupedNotification extends ListNotifications.Notification { @@ -242,9 +242,10 @@ export class NotificationsViewModel { private async _initialLoad(isRefreshing = false) { this._xLoading(isRefreshing) try { - const res = await this.rootStore.api.app.bsky.notification.list( - this.params, - ) + const params = Object.assign({}, this.params, { + limit: PAGE_SIZE, + }) + const res = await this.rootStore.api.app.bsky.notification.list(params) this._replaceAll(res) this._xIdle() } catch (e: any) { @@ -259,6 +260,7 @@ export class NotificationsViewModel { this._xLoading() try { const params = Object.assign({}, this.params, { + limit: PAGE_SIZE, before: this.loadMoreCursor, }) const res = await this.rootStore.api.app.bsky.notification.list(params) diff --git a/src/view/com/composer/ComposePost.tsx b/src/view/com/composer/ComposePost.tsx index 10305adb6..ce42ee17e 100644 --- a/src/view/com/composer/ComposePost.tsx +++ b/src/view/com/composer/ComposePost.tsx @@ -1,4 +1,4 @@ -import React, {useEffect, useMemo, useState} from 'react' +import React, {useEffect, useMemo, useRef, useState} from 'react' import {observer} from 'mobx-react-lite' import { ActivityIndicator, @@ -17,9 +17,10 @@ import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {UserAutocompleteViewModel} from '../../../state/models/user-autocomplete-view' import {UserLocalPhotosModel} from '../../../state/models/user-local-photos' import {Autocomplete} from './Autocomplete' -import Toast from '../util/Toast' +import * as Toast from '../util/Toast' import ProgressCircle from '../util/ProgressCircle' import {TextLink} from '../util/Link' +import {UserAvatar} from '../util/UserAvatar' import {useStores} from '../../../state' import * as apilib from '../../../state/lib/api' import {ComposerOpts} from '../../../state/models/shell-ui' @@ -28,7 +29,6 @@ import {detectLinkables} from '../../../lib/strings' import {openPicker, openCamera} from 'react-native-image-crop-picker' const MAX_TEXT_LENGTH = 256 -const WARNING_TEXT_LENGTH = 200 const DANGER_TEXT_LENGTH = MAX_TEXT_LENGTH export const ComposePost = observer(function ComposePost({ @@ -41,6 +41,7 @@ export const ComposePost = observer(function ComposePost({ onClose: () => void }) { const store = useStores() + const textInput = useRef<TextInput>(null) const [isProcessing, setIsProcessing] = useState(false) const [error, setError] = useState('') const [text, setText] = useState('') @@ -57,6 +58,22 @@ export const ComposePost = observer(function ComposePost({ useEffect(() => { autocompleteView.setup() }) + useEffect(() => { + // HACK + // wait a moment before focusing the input to resolve some layout bugs with the keyboard-avoiding-view + // -prf + let to: NodeJS.Timeout | undefined + if (textInput.current) { + to = setTimeout(() => { + textInput.current?.focus() + }, 250) + } + return () => { + if (to) { + clearTimeout(to) + } + } + }, [textInput.current]) useEffect(() => { localPhotos.setup() @@ -90,7 +107,10 @@ export const ComposePost = observer(function ComposePost({ } setIsProcessing(true) try { - await apilib.post(store, text, replyTo, autocompleteView.knownHandles) + const replyRef = replyTo + ? {uri: replyTo.uri, cid: replyTo.cid} + : undefined + await apilib.post(store, text, replyRef, autocompleteView.knownHandles) } catch (e: any) { console.error(`Failed to create post: ${e.toString()}`) setError( @@ -101,13 +121,7 @@ export const ComposePost = observer(function ComposePost({ } onPost?.() onClose() - Toast.show(`Your ${replyTo ? 'reply' : 'post'} has been published`, { - duration: Toast.durations.LONG, - position: Toast.positions.TOP, - shadow: true, - animation: true, - hideOnPress: true, - }) + Toast.show(`Your ${replyTo ? 'reply' : 'post'} has been published`) } const onSelectAutocompleteItem = (item: string) => { setText(replaceTextAutocompletePrefix(text, item)) @@ -115,12 +129,7 @@ export const ComposePost = observer(function ComposePost({ } const canPost = text.length <= MAX_TEXT_LENGTH - const progressColor = - text.length > DANGER_TEXT_LENGTH - ? '#e60000' - : text.length > WARNING_TEXT_LENGTH - ? '#f7c600' - : undefined + const progressColor = text.length > DANGER_TEXT_LENGTH ? '#e60000' : undefined const textDecorated = useMemo(() => { let i = 0 @@ -142,7 +151,7 @@ export const ComposePost = observer(function ComposePost({ <SafeAreaView style={s.flex1}> <View style={styles.topbar}> <TouchableOpacity onPress={onPressCancel}> - <Text style={[s.blue3, s.f16]}>Cancel</Text> + <Text style={[s.blue3, s.f18]}>Cancel</Text> </TouchableOpacity> <View style={s.flex1} /> {isProcessing ? ( @@ -156,7 +165,9 @@ export const ComposePost = observer(function ComposePost({ start={{x: 0, y: 0}} end={{x: 1, y: 1}} style={styles.postBtn}> - <Text style={[s.white, s.f16, s.bold]}>Post</Text> + <Text style={[s.white, s.f16, s.bold]}> + {replyTo ? 'Reply' : 'Post'} + </Text> </LinearGradient> </TouchableOpacity> ) : ( @@ -178,39 +189,46 @@ export const ComposePost = observer(function ComposePost({ </View> )} {replyTo ? ( - <View> - <Text style={s.gray4}> - Replying to{' '} + <View style={styles.replyToLayout}> + <UserAvatar + handle={replyTo.author.handle} + displayName={replyTo.author.displayName} + size={50} + /> + <View style={styles.replyToPost}> <TextLink href={`/profile/${replyTo.author.handle}`} - text={'@' + replyTo.author.handle} - style={[s.bold, s.gray5]} + text={replyTo.author.displayName || replyTo.author.handle} + style={[s.f16, s.bold]} /> - </Text> - <View style={styles.replyToPost}> - <Text style={s.gray5}>{replyTo.text}</Text> + <Text style={[s.f16, s['lh16-1.3']]} numberOfLines={6}> + {replyTo.text} + </Text> </View> </View> ) : undefined} - <TextInput - multiline - scrollEnabled - onChangeText={(text: string) => onChangeText(text)} - placeholder={ - replyTo - ? 'Write your reply' - : photoUris.length === 0 - ? "What's up?" - : 'Add a comment...' - } - style={styles.textInput}> - {textDecorated} - </TextInput> + <View style={styles.textInputLayout}> + <UserAvatar + handle={store.me.handle || ''} + displayName={store.me.displayName} + size={50} + /> + <TextInput + ref={textInput} + multiline + scrollEnabled + onChangeText={(text: string) => onChangeText(text)} + placeholder={replyTo ? 'Write your reply' : "What's up?"} + style={styles.textInput}> + {textDecorated} + </TextInput> + </View> {photoUris.length !== 0 && ( <View style={styles.selectedImageContainer}> {photoUris.length !== 0 && - photoUris.map(item => ( + photoUris.map((item, index) => ( <View + key={`selected-image-${index}`} style={[ styles.selectedImage, photoUris.length === 1 @@ -264,8 +282,9 @@ export const ComposePost = observer(function ComposePost({ style={{color: colors.blue3}} /> </TouchableOpacity> - {localPhotos.photos.map(item => ( + {localPhotos.photos.map((item, index) => ( <TouchableOpacity + key={`local-image-${index}`} style={styles.photoButton} onPress={() => { setPhotoUris([item.node.image.uri, ...photoUris]) @@ -343,9 +362,9 @@ const styles = StyleSheet.create({ flexDirection: 'row', alignItems: 'center', paddingTop: 10, - paddingBottom: 5, + paddingBottom: 10, paddingHorizontal: 5, - height: 50, + height: 55, }, postBtn: { borderRadius: 20, @@ -371,19 +390,30 @@ const styles = StyleSheet.create({ justifyContent: 'center', marginRight: 5, }, + textInputLayout: { + flexDirection: 'row', + flex: 1, + borderTopWidth: 1, + borderTopColor: colors.gray2, + paddingTop: 16, + }, textInput: { flex: 1, padding: 5, - fontSize: 21, + fontSize: 18, + marginLeft: 8, + }, + replyToLayout: { + flexDirection: 'row', + borderTopWidth: 1, + borderTopColor: colors.gray2, + paddingTop: 16, + paddingBottom: 16, }, replyToPost: { - paddingHorizontal: 8, - paddingVertical: 6, - borderWidth: 1, - borderColor: colors.gray2, - borderRadius: 6, - marginTop: 5, - marginBottom: 10, + flex: 1, + paddingLeft: 13, + paddingRight: 8, }, contentCenter: {alignItems: 'center'}, selectedImageContainer: { diff --git a/src/view/com/composer/Prompt.tsx b/src/view/com/composer/Prompt.tsx index f9fd7e7d3..7805e00dd 100644 --- a/src/view/com/composer/Prompt.tsx +++ b/src/view/com/composer/Prompt.tsx @@ -1,29 +1,42 @@ import React from 'react' import {StyleSheet, Text, TouchableOpacity, View} from 'react-native' -import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {colors} from '../../lib/styles' import {useStores} from '../../../state' import {UserAvatar} from '../util/UserAvatar' -export function ComposePrompt({onPressCompose}: {onPressCompose: () => void}) { +export function ComposePrompt({ + noAvi = false, + text = "What's up?", + btn = 'Post', + onPressCompose, +}: { + noAvi?: boolean + text?: string + btn?: string + onPressCompose: () => void +}) { const store = useStores() const onPressAvatar = () => { store.nav.navigate(`/profile/${store.me.handle}`) } return ( - <TouchableOpacity style={styles.container} onPress={onPressCompose}> - <TouchableOpacity style={styles.avatar} onPress={onPressAvatar}> - <UserAvatar - size={50} - handle={store.me.handle || ''} - displayName={store.me.displayName} - /> - </TouchableOpacity> + <TouchableOpacity + style={[styles.container, noAvi ? styles.noAviContainer : undefined]} + onPress={onPressCompose}> + {!noAvi ? ( + <TouchableOpacity style={styles.avatar} onPress={onPressAvatar}> + <UserAvatar + size={50} + handle={store.me.handle || ''} + displayName={store.me.displayName} + /> + </TouchableOpacity> + ) : undefined} <View style={styles.textContainer}> - <Text style={styles.text}>What's up?</Text> + <Text style={styles.text}>{text}</Text> </View> <View style={styles.btn}> - <Text style={styles.btnText}>Post</Text> + <Text style={styles.btnText}>{btn}</Text> </View> </TouchableOpacity> ) @@ -40,6 +53,9 @@ const styles = StyleSheet.create({ alignItems: 'center', backgroundColor: colors.white, }, + noAviContainer: { + paddingVertical: 14, + }, avatar: { width: 50, }, diff --git a/src/view/com/discover/SuggestedFollows.tsx b/src/view/com/discover/SuggestedFollows.tsx index d8cb0c4db..d5875f0f7 100644 --- a/src/view/com/discover/SuggestedFollows.tsx +++ b/src/view/com/discover/SuggestedFollows.tsx @@ -14,7 +14,7 @@ import _omit from 'lodash.omit' import {ErrorScreen} from '../util/ErrorScreen' import {Link} from '../util/Link' import {UserAvatar} from '../util/UserAvatar' -import Toast from '../util/Toast' +import * as Toast from '../util/Toast' import {useStores} from '../../../state' import * as apilib from '../../../state/lib/api' import { @@ -63,10 +63,7 @@ export const SuggestedFollows = observer( setFollows({[item.did]: res.uri, ...follows}) } catch (e) { console.log(e) - Toast.show('An issue occurred, please try again.', { - duration: Toast.durations.LONG, - position: Toast.positions.TOP, - }) + Toast.show('An issue occurred, please try again.') } } const onPressUnfollow = async (item: SuggestedActor) => { @@ -75,10 +72,7 @@ export const SuggestedFollows = observer( setFollows(_omit(follows, [item.did])) } catch (e) { console.log(e) - Toast.show('An issue occurred, please try again.', { - duration: Toast.durations.LONG, - position: Toast.positions.TOP, - }) + Toast.show('An issue occurred, please try again.') } } diff --git a/src/view/com/modals/CreateScene.tsx b/src/view/com/modals/CreateScene.tsx index 445374623..646d5b242 100644 --- a/src/view/com/modals/CreateScene.tsx +++ b/src/view/com/modals/CreateScene.tsx @@ -1,5 +1,5 @@ import React, {useState} from 'react' -import Toast from '../util/Toast' +import * as Toast from '../util/Toast' import { ActivityIndicator, StyleSheet, @@ -71,9 +71,7 @@ export function Component({}: {}) { }, ) .catch(e => console.error(e)) // an error here is not critical - Toast.show('Scene created', { - position: Toast.positions.TOP, - }) + Toast.show('Scene created') store.shell.closeModal() store.nav.navigate(`/profile/${fullHandle}`) } catch (e: any) { diff --git a/src/view/com/modals/EditProfile.tsx b/src/view/com/modals/EditProfile.tsx index f739b0843..50acccf67 100644 --- a/src/view/com/modals/EditProfile.tsx +++ b/src/view/com/modals/EditProfile.tsx @@ -1,5 +1,5 @@ import React, {useState} from 'react' -import Toast from '../util/Toast' +import * as Toast from '../util/Toast' import {StyleSheet, Text, TouchableOpacity, View} from 'react-native' import LinearGradient from 'react-native-linear-gradient' import {BottomSheetScrollView, BottomSheetTextInput} from '@gorhom/bottom-sheet' @@ -52,9 +52,7 @@ export function Component({ } }, ) - Toast.show('Profile updated', { - position: Toast.positions.TOP, - }) + Toast.show('Profile updated') onUpdate?.() store.shell.closeModal() } catch (e: any) { diff --git a/src/view/com/modals/InviteToScene.tsx b/src/view/com/modals/InviteToScene.tsx index 2d4e372c1..8df38daf0 100644 --- a/src/view/com/modals/InviteToScene.tsx +++ b/src/view/com/modals/InviteToScene.tsx @@ -1,6 +1,6 @@ import React, {useState, useEffect, useMemo} from 'react' import {observer} from 'mobx-react-lite' -import Toast from '../util/Toast' +import * as Toast from '../util/Toast' import { ActivityIndicator, FlatList, @@ -83,10 +83,7 @@ export const Component = observer(function Component({ follow.declaration.cid, ) setCreatedInvites({[follow.did]: assertionUri, ...createdInvites}) - Toast.show('Invite sent', { - duration: Toast.durations.LONG, - position: Toast.positions.TOP, - }) + Toast.show('Invite sent') } catch (e) { setError('There was an issue with the invite. Please try again.') console.error(e) @@ -119,10 +116,7 @@ export const Component = observer(function Component({ [assertion.uri]: true, ...deletedPendingInvites, }) - Toast.show('Invite removed', { - duration: Toast.durations.LONG, - position: Toast.positions.TOP, - }) + Toast.show('Invite removed') } catch (e) { setError('There was an issue with the invite. Please try again.') console.error(e) diff --git a/src/view/com/notifications/InviteAccepter.tsx b/src/view/com/notifications/InviteAccepter.tsx index 7d735a66b..72bc06764 100644 --- a/src/view/com/notifications/InviteAccepter.tsx +++ b/src/view/com/notifications/InviteAccepter.tsx @@ -7,7 +7,7 @@ import {NotificationsViewItemModel} from '../../../state/models/notifications-vi import {ConfirmModel} from '../../../state/models/shell-ui' import {useStores} from '../../../state' import {ProfileCard} from '../profile/ProfileCard' -import Toast from '../util/Toast' +import * as Toast from '../util/Toast' import {s, colors, gradients} from '../../lib/styles' export function InviteAccepter({item}: {item: NotificationsViewItemModel}) { @@ -46,10 +46,7 @@ export function InviteAccepter({item}: {item: NotificationsViewItemModel}) { }, }) store.me.refreshMemberships() - Toast.show('Invite accepted', { - duration: Toast.durations.LONG, - position: Toast.positions.TOP, - }) + Toast.show('Invite accepted') setConfirmationUri(uri) } return ( diff --git a/src/view/com/post-thread/PostThreadItem.tsx b/src/view/com/post-thread/PostThreadItem.tsx index 95b02837d..85c241ce4 100644 --- a/src/view/com/post-thread/PostThreadItem.tsx +++ b/src/view/com/post-thread/PostThreadItem.tsx @@ -8,7 +8,7 @@ import {PostThreadViewPostModel} from '../../../state/models/post-thread-view' import {Link} from '../util/Link' import {RichText} from '../util/RichText' import {PostDropdownBtn} from '../util/DropdownBtn' -import Toast from '../util/Toast' +import * as Toast from '../util/Toast' import {UserAvatar} from '../util/UserAvatar' import {s, colors} from '../../lib/styles' import {ago, pluralize} from '../../../lib/strings' @@ -16,6 +16,7 @@ import {useStores} from '../../../state' import {PostMeta} from '../util/PostMeta' import {PostEmbeds} from '../util/PostEmbeds' import {PostCtrls} from '../util/PostCtrls' +import {ComposePrompt} from '../composer/Prompt' const PARENT_REPLY_LINE_LENGTH = 8 const REPLYING_TO_LINE_LENGTH = 6 @@ -78,131 +79,133 @@ export const PostThreadItem = observer(function PostThreadItem({ item.delete().then( () => { setDeleted(true) - Toast.show('Post deleted', { - position: Toast.positions.TOP, - }) + Toast.show('Post deleted') }, e => { console.error(e) - Toast.show('Failed to delete post, please try again', { - position: Toast.positions.TOP, - }) + Toast.show('Failed to delete post, please try again') }, ) } if (item._isHighlightedPost) { return ( - <View style={styles.outer}> - <View style={styles.layout}> - <View style={styles.layoutAvi}> - <Link href={authorHref} title={authorTitle}> - <UserAvatar - size={50} - displayName={item.author.displayName} - handle={item.author.handle} - /> - </Link> - </View> - <View style={styles.layoutContent}> - <View style={[styles.meta, {paddingTop: 5, paddingBottom: 0}]}> - <Link - style={styles.metaItem} - href={authorHref} - title={authorTitle}> - <Text style={[s.f16, s.bold]} numberOfLines={1}> - {item.author.displayName || item.author.handle} - </Text> - </Link> - <Text style={[styles.metaItem, s.f15, s.gray5]}> - · {ago(item.indexedAt)} - </Text> - <View style={s.flex1} /> - <PostDropdownBtn - style={styles.metaItem} - itemHref={itemHref} - itemTitle={itemTitle} - isAuthor={item.author.did === store.me.did} - onDeletePost={onDeletePost}> - <FontAwesomeIcon - icon="ellipsis-h" - size={14} - style={[s.mt2, s.mr5]} + <> + <View style={styles.outer}> + <View style={styles.layout}> + <View style={styles.layoutAvi}> + <Link href={authorHref} title={authorTitle}> + <UserAvatar + size={50} + displayName={item.author.displayName} + handle={item.author.handle} /> - </PostDropdownBtn> - </View> - <View style={styles.meta}> - <Link - style={styles.metaItem} - href={authorHref} - title={authorTitle}> - <Text style={[s.f15, s.gray5]} numberOfLines={1}> - @{item.author.handle} - </Text> </Link> </View> - </View> - </View> - <View style={[s.pl10, s.pr10, s.pb10]}> - <View - style={[styles.postTextContainer, styles.postTextLargeContainer]}> - <RichText - text={record.text} - entities={record.entities} - style={[styles.postText, styles.postTextLarge]} - /> - </View> - <PostEmbeds entities={record.entities} /> - {item._isHighlightedPost && hasEngagement ? ( - <View style={styles.expandedInfo}> - {item.repostCount ? ( + <View style={styles.layoutContent}> + <View style={[styles.meta, {paddingTop: 5, paddingBottom: 0}]}> <Link - style={styles.expandedInfoItem} - href={repostsHref} - title={repostsTitle}> - <Text style={[s.gray5, s.semiBold, s.f18]}> - <Text style={[s.bold, s.black, s.f18]}> - {item.repostCount} - </Text>{' '} - {pluralize(item.repostCount, 'repost')} + style={styles.metaItem} + href={authorHref} + title={authorTitle}> + <Text style={[s.f16, s.bold]} numberOfLines={1}> + {item.author.displayName || item.author.handle} </Text> </Link> - ) : ( - <></> - )} - {item.upvoteCount ? ( + <Text style={[styles.metaItem, s.f15, s.gray5]}> + · {ago(item.indexedAt)} + </Text> + <View style={s.flex1} /> + <PostDropdownBtn + style={styles.metaItem} + itemHref={itemHref} + itemTitle={itemTitle} + isAuthor={item.author.did === store.me.did} + onDeletePost={onDeletePost}> + <FontAwesomeIcon + icon="ellipsis-h" + size={14} + style={[s.mt2, s.mr5]} + /> + </PostDropdownBtn> + </View> + <View style={styles.meta}> <Link - style={styles.expandedInfoItem} - href={upvotesHref} - title={upvotesTitle}> - <Text style={[s.gray5, s.semiBold, s.f18]}> - <Text style={[s.bold, s.black, s.f18]}> - {item.upvoteCount} - </Text>{' '} - {pluralize(item.upvoteCount, 'upvote')} + style={styles.metaItem} + href={authorHref} + title={authorTitle}> + <Text style={[s.f15, s.gray5]} numberOfLines={1}> + @{item.author.handle} </Text> </Link> - ) : ( - <></> - )} + </View> + </View> + </View> + <View style={[s.pl10, s.pr10, s.pb10]}> + <View + style={[styles.postTextContainer, styles.postTextLargeContainer]}> + <RichText + text={record.text} + entities={record.entities} + style={[styles.postText, styles.postTextLarge]} + /> + </View> + <PostEmbeds entities={record.entities} style={s.mb10} /> + {item._isHighlightedPost && hasEngagement ? ( + <View style={styles.expandedInfo}> + {item.repostCount ? ( + <Link + style={styles.expandedInfoItem} + href={repostsHref} + title={repostsTitle}> + <Text style={[s.gray5, s.semiBold, s.f17]}> + <Text style={[s.bold, s.black, s.f17]}> + {item.repostCount} + </Text>{' '} + {pluralize(item.repostCount, 'repost')} + </Text> + </Link> + ) : ( + <></> + )} + {item.upvoteCount ? ( + <Link + style={styles.expandedInfoItem} + href={upvotesHref} + title={upvotesTitle}> + <Text style={[s.gray5, s.semiBold, s.f17]}> + <Text style={[s.bold, s.black, s.f17]}> + {item.upvoteCount} + </Text>{' '} + {pluralize(item.upvoteCount, 'upvote')} + </Text> + </Link> + ) : ( + <></> + )} + </View> + ) : ( + <></> + )} + <View style={[s.pl10, s.pb5]}> + <PostCtrls + big + isReposted={!!item.myState.repost} + isUpvoted={!!item.myState.upvote} + onPressReply={onPressReply} + onPressToggleRepost={onPressToggleRepost} + onPressToggleUpvote={onPressToggleUpvote} + /> </View> - ) : ( - <></> - )} - <View style={[s.pl10]}> - <PostCtrls - replyCount={item.replyCount} - repostCount={item.repostCount} - upvoteCount={item.upvoteCount} - isReposted={!!item.myState.repost} - isUpvoted={!!item.myState.upvote} - onPressReply={onPressReply} - onPressToggleRepost={onPressToggleRepost} - onPressToggleUpvote={onPressToggleUpvote} - /> </View> </View> - </View> + <ComposePrompt + noAvi + text="Write your reply" + btn="Reply" + onPressCompose={onPressReply} + /> + </> ) } else { return ( @@ -345,8 +348,8 @@ const styles = StyleSheet.create({ }, postText: { fontFamily: 'Helvetica Neue', - fontSize: 17, - lineHeight: 22.1, // 1.3 of 17px + fontSize: 16, + lineHeight: 20.8, // 1.3 of 16px }, postTextContainer: { flexDirection: 'row', @@ -371,7 +374,7 @@ const styles = StyleSheet.create({ borderTopWidth: 1, borderBottomWidth: 1, marginTop: 5, - marginBottom: 10, + marginBottom: 15, }, expandedInfoItem: { marginRight: 10, diff --git a/src/view/com/post/Post.tsx b/src/view/com/post/Post.tsx index d0df1b295..4d668cac3 100644 --- a/src/view/com/post/Post.tsx +++ b/src/view/com/post/Post.tsx @@ -10,7 +10,7 @@ import {UserInfoText} from '../util/UserInfoText' import {PostMeta} from '../util/PostMeta' import {PostCtrls} from '../util/PostCtrls' import {RichText} from '../util/RichText' -import Toast from '../util/Toast' +import * as Toast from '../util/Toast' import {UserAvatar} from '../util/UserAvatar' import {useStores} from '../../../state' import {s, colors} from '../../lib/styles' @@ -99,15 +99,11 @@ export const Post = observer(function Post({uri}: {uri: string}) { item.delete().then( () => { setDeleted(true) - Toast.show('Post deleted', { - position: Toast.positions.TOP, - }) + Toast.show('Post deleted') }, e => { console.error(e) - Toast.show('Failed to delete post, please try again', { - position: Toast.positions.TOP, - }) + Toast.show('Failed to delete post, please try again') }, ) } @@ -196,7 +192,7 @@ const styles = StyleSheet.create({ }, postText: { fontFamily: 'Helvetica Neue', - fontSize: 17, - lineHeight: 22.1, // 1.3 of 17px + fontSize: 16, + lineHeight: 20.8, // 1.3 of 16px }, }) diff --git a/src/view/com/posts/FeedItem.tsx b/src/view/com/posts/FeedItem.tsx index 4063b2008..4d50531bd 100644 --- a/src/view/com/posts/FeedItem.tsx +++ b/src/view/com/posts/FeedItem.tsx @@ -11,7 +11,7 @@ import {PostMeta} from '../util/PostMeta' import {PostCtrls} from '../util/PostCtrls' import {PostEmbeds} from '../util/PostEmbeds' import {RichText} from '../util/RichText' -import Toast from '../util/Toast' +import * as Toast from '../util/Toast' import {UserAvatar} from '../util/UserAvatar' import {s, colors} from '../../lib/styles' import {useStores} from '../../../state' @@ -70,15 +70,11 @@ export const FeedItem = observer(function FeedItem({ item.delete().then( () => { setDeleted(true) - Toast.show('Post deleted', { - position: Toast.positions.TOP, - }) + Toast.show('Post deleted') }, e => { console.error(e) - Toast.show('Failed to delete post, please try again', { - position: Toast.positions.TOP, - }) + Toast.show('Failed to delete post, please try again') }, ) } @@ -254,7 +250,7 @@ const styles = StyleSheet.create({ }, postText: { fontFamily: 'Helvetica Neue', - fontSize: 17, - lineHeight: 22.1, // 1.3 of 17px + fontSize: 16, + lineHeight: 20.8, // 1.3 of 16px }, }) diff --git a/src/view/com/profile/ProfileHeader.tsx b/src/view/com/profile/ProfileHeader.tsx index 9325a88a3..1b25c7c13 100644 --- a/src/view/com/profile/ProfileHeader.tsx +++ b/src/view/com/profile/ProfileHeader.tsx @@ -1,12 +1,6 @@ import React, {useMemo} from 'react' import {observer} from 'mobx-react-lite' -import { - ActivityIndicator, - StyleSheet, - Text, - TouchableOpacity, - View, -} from 'react-native' +import {StyleSheet, Text, TouchableOpacity, View} from 'react-native' import LinearGradient from 'react-native-linear-gradient' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {AtUri} from '../../../third-party/uri' @@ -20,9 +14,8 @@ import { import {pluralize} from '../../../lib/strings' import {s, colors} from '../../lib/styles' import {getGradient} from '../../lib/asset-gen' -import {MagnifyingGlassIcon} from '../../lib/icons' import {DropdownBtn, DropdownItem} from '../util/DropdownBtn' -import Toast from '../util/Toast' +import * as Toast from '../util/Toast' import {LoadingPlaceholder} from '../util/LoadingPlaceholder' import {RichText} from '../util/RichText' import {UserAvatar} from '../util/UserAvatar' @@ -55,10 +48,6 @@ export const ProfileHeader = observer(function ProfileHeader({ `${view.myState.follow ? 'Following' : 'No longer following'} ${ view.displayName || view.handle }`, - { - duration: Toast.durations.LONG, - position: Toast.positions.TOP, - }, ) }, err => console.error('Failed to toggle follow', err), @@ -94,10 +83,7 @@ export const ProfileHeader = observer(function ProfileHeader({ did: store.me.did || '', rkey: new AtUri(view.myState.member).rkey, }) - Toast.show(`Scene left`, { - duration: Toast.durations.LONG, - position: Toast.positions.TOP, - }) + Toast.show(`Scene left`) } onRefreshAll() } @@ -108,18 +94,6 @@ export const ProfileHeader = observer(function ProfileHeader({ return ( <View style={styles.outer}> <LoadingPlaceholder width="100%" height={120} /> - {store.nav.tab.canGoBack ? ( - <TouchableOpacity style={styles.backButton} onPress={onPressBack}> - <FontAwesomeIcon - size={18} - icon="angle-left" - style={styles.backIcon} - /> - </TouchableOpacity> - ) : undefined} - <TouchableOpacity style={styles.searchBtn} onPress={onPressSearch}> - <MagnifyingGlassIcon size={19} style={styles.searchIcon} /> - </TouchableOpacity> <View style={styles.avi}> <LoadingPlaceholder width={80} @@ -179,18 +153,6 @@ export const ProfileHeader = observer(function ProfileHeader({ return ( <View style={styles.outer}> <UserBanner handle={view.handle} /> - {store.nav.tab.canGoBack ? ( - <TouchableOpacity style={styles.backButton} onPress={onPressBack}> - <FontAwesomeIcon - size={18} - icon="angle-left" - style={styles.backIcon} - /> - </TouchableOpacity> - ) : undefined} - <TouchableOpacity style={styles.searchBtn} onPress={onPressSearch}> - <MagnifyingGlassIcon size={19} style={styles.searchIcon} /> - </TouchableOpacity> <View style={styles.avi}> <UserAvatar size={80} @@ -353,30 +315,6 @@ const styles = StyleSheet.create({ width: '100%', height: 120, }, - backButton: { - position: 'absolute', - top: 10, - left: 12, - backgroundColor: '#ffff', - padding: 6, - borderRadius: 30, - }, - backIcon: { - width: 14, - height: 14, - color: colors.black, - }, - searchBtn: { - position: 'absolute', - top: 10, - right: 12, - backgroundColor: '#ffff', - padding: 5, - borderRadius: 30, - }, - searchIcon: { - color: colors.black, - }, avi: { position: 'absolute', top: 80, diff --git a/src/view/com/util/PostCtrls.tsx b/src/view/com/util/PostCtrls.tsx index a316d8959..10b54be3f 100644 --- a/src/view/com/util/PostCtrls.tsx +++ b/src/view/com/util/PostCtrls.tsx @@ -12,9 +12,10 @@ import {UpIcon, UpIconSolid} from '../../lib/icons' import {s, colors} from '../../lib/styles' interface PostCtrlsOpts { - replyCount: number - repostCount: number - upvoteCount: number + big?: boolean + replyCount?: number + repostCount?: number + upvoteCount?: number isReposted: boolean isUpvoted: boolean onPressReply: () => void @@ -30,17 +31,17 @@ export function PostCtrls(opts: PostCtrlsOpts) { const interp2 = useSharedValue<number>(0) const anim1Style = useAnimatedStyle(() => ({ - transform: [{scale: interpolate(interp1.value, [0, 1.0], [1.0, 3.0])}], + transform: [{scale: interpolate(interp1.value, [0, 1.0], [1.0, 4.0])}], opacity: interpolate(interp1.value, [0, 1.0], [1.0, 0.0]), })) const anim2Style = useAnimatedStyle(() => ({ - transform: [{scale: interpolate(interp2.value, [0, 1.0], [1.0, 3.0])}], + transform: [{scale: interpolate(interp2.value, [0, 1.0], [1.0, 4.0])}], opacity: interpolate(interp2.value, [0, 1.0], [1.0, 0.0]), })) const onPressToggleRepostWrapper = () => { if (!opts.isReposted) { - interp1.value = withTiming(1, {duration: 300}, () => { + interp1.value = withTiming(1, {duration: 400}, () => { interp1.value = withDelay(100, withTiming(0, {duration: 20})) }) } @@ -48,7 +49,7 @@ export function PostCtrls(opts: PostCtrlsOpts) { } const onPressToggleUpvoteWrapper = () => { if (!opts.isUpvoted) { - interp2.value = withTiming(1, {duration: 300}, () => { + interp2.value = withTiming(1, {duration: 400}, () => { interp2.value = withDelay(100, withTiming(0, {duration: 20})) }) } @@ -62,9 +63,11 @@ export function PostCtrls(opts: PostCtrlsOpts) { <FontAwesomeIcon style={styles.ctrlIcon} icon={['far', 'comment']} - size={14} + size={opts.big ? 20 : 14} /> - <Text style={[sRedgray, s.ml5, s.f16]}>{opts.replyCount}</Text> + {typeof opts.replyCount !== 'undefined' ? ( + <Text style={[sRedgray, s.ml5, s.f16]}>{opts.replyCount}</Text> + ) : undefined} </TouchableOpacity> </View> <View style={s.flex1}> @@ -77,17 +80,19 @@ export function PostCtrls(opts: PostCtrlsOpts) { opts.isReposted ? styles.ctrlIconReposted : styles.ctrlIcon } icon="retweet" - size={18} + size={opts.big ? 22 : 18} /> </Animated.View> - <Text - style={ - opts.isReposted - ? [s.bold, s.green3, s.f16, s.ml5] - : [sRedgray, s.f16, s.ml5] - }> - {opts.repostCount} - </Text> + {typeof opts.repostCount !== 'undefined' ? ( + <Text + style={ + opts.isReposted + ? [s.bold, s.green3, s.f16, s.ml5] + : [sRedgray, s.f16, s.ml5] + }> + {opts.repostCount} + </Text> + ) : undefined} </TouchableOpacity> </View> <View style={s.flex1}> @@ -96,19 +101,28 @@ export function PostCtrls(opts: PostCtrlsOpts) { onPress={onPressToggleUpvoteWrapper}> <Animated.View style={anim2Style}> {opts.isUpvoted ? ( - <UpIconSolid style={[styles.ctrlIconUpvoted]} size={18} /> + <UpIconSolid + style={[styles.ctrlIconUpvoted]} + size={opts.big ? 22 : 18} + /> ) : ( - <UpIcon style={[styles.ctrlIcon]} size={18} strokeWidth={1.5} /> + <UpIcon + style={[styles.ctrlIcon]} + size={opts.big ? 22 : 18} + strokeWidth={1.5} + /> )} </Animated.View> - <Text - style={ - opts.isUpvoted - ? [s.bold, s.red3, s.f16, s.ml5] - : [sRedgray, s.f16, s.ml5] - }> - {opts.upvoteCount} - </Text> + {typeof opts.upvoteCount !== 'undefined' ? ( + <Text + style={ + opts.isUpvoted + ? [s.bold, s.red3, s.f16, s.ml5] + : [sRedgray, s.f16, s.ml5] + }> + {opts.upvoteCount} + </Text> + ) : undefined} </TouchableOpacity> </View> <View style={s.flex1}></View> diff --git a/src/view/com/util/Toast.native.tsx b/src/view/com/util/Toast.native.tsx deleted file mode 100644 index 4b9fd7f80..000000000 --- a/src/view/com/util/Toast.native.tsx +++ /dev/null @@ -1,2 +0,0 @@ -import Toast from 'react-native-root-toast' -export default Toast diff --git a/src/view/com/util/Toast.tsx b/src/view/com/util/Toast.tsx index 1726b71b3..197f47422 100644 --- a/src/view/com/util/Toast.tsx +++ b/src/view/com/util/Toast.tsx @@ -1,62 +1,11 @@ -/* - * Note: the dataSet properties are used to leverage custom CSS in public/index.html - */ - -import React, {useState, useEffect} from 'react' -// @ts-ignore no declarations available -prf -import {Text, View} from 'react-native-web' -import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' - -interface ActiveToast { - text: string -} -type GlobalSetActiveToast = (_activeToast: ActiveToast | undefined) => void - -// globals -// = -let globalSetActiveToast: GlobalSetActiveToast | undefined -let toastTimeout: NodeJS.Timeout | undefined - -// components -// = -type ToastContainerProps = {} -const ToastContainer: React.FC<ToastContainerProps> = ({}) => { - const [activeToast, setActiveToast] = useState<ActiveToast | undefined>() - useEffect(() => { - globalSetActiveToast = (t: ActiveToast | undefined) => { - setActiveToast(t) - } +import Toast from 'react-native-root-toast' + +export function show(message: string) { + Toast.show(message, { + duration: Toast.durations.LONG, + position: 50, + shadow: true, + animation: true, + hideOnPress: true, }) - return ( - <> - {activeToast && ( - <View dataSet={{'toast-container': 1}}> - <FontAwesomeIcon icon="check" size={24} /> - <Text>{activeToast.text}</Text> - </View> - )} - </> - ) -} - -// exports -// = -export default { - show(text: string, _opts: any) { - console.log('TODO: toast', text) - if (toastTimeout) { - clearTimeout(toastTimeout) - } - globalSetActiveToast?.({text}) - toastTimeout = setTimeout(() => { - globalSetActiveToast?.(undefined) - }, 2e3) - }, - positions: { - TOP: 0, - }, - durations: { - LONG: 0, - }, - ToastContainer, } diff --git a/src/view/com/util/ViewHeader.tsx b/src/view/com/util/ViewHeader.tsx index 55a71ea26..50b7e6532 100644 --- a/src/view/com/util/ViewHeader.tsx +++ b/src/view/com/util/ViewHeader.tsx @@ -1,7 +1,6 @@ import React from 'react' import {StyleSheet, Text, TouchableOpacity, View} from 'react-native' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' -import {UserAvatar} from './UserAvatar' import {colors} from '../../lib/styles' import {MagnifyingGlassIcon} from '../../lib/icons' import {useStores} from '../../../state' @@ -9,14 +8,19 @@ import {useStores} from '../../../state' export function ViewHeader({ title, subtitle, + onPost, }: { title: string subtitle?: string + onPost?: () => void }) { const store = useStores() const onPressBack = () => { store.nav.tab.goBack() } + const onPressCompose = () => { + store.shell.openComposer({onPost}) + } const onPressSearch = () => { store.nav.navigate(`/search`) } @@ -26,9 +30,7 @@ export function ViewHeader({ <TouchableOpacity onPress={onPressBack} style={styles.backIcon}> <FontAwesomeIcon size={18} icon="angle-left" style={{marginTop: 6}} /> </TouchableOpacity> - ) : ( - <View style={styles.cornerPlaceholder} /> - )} + ) : undefined} <View style={styles.titleContainer}> <Text style={styles.title}>{title}</Text> {subtitle ? ( @@ -37,8 +39,17 @@ export function ViewHeader({ </Text> ) : undefined} </View> - <TouchableOpacity onPress={onPressSearch} style={styles.searchBtn}> - <MagnifyingGlassIcon size={17} style={styles.searchBtnIcon} /> + <TouchableOpacity onPress={onPressCompose} style={styles.btn}> + <FontAwesomeIcon size={18} icon="plus" /> + </TouchableOpacity> + <TouchableOpacity + onPress={onPressSearch} + style={[styles.btn, {marginLeft: 8}]}> + <MagnifyingGlassIcon + size={18} + strokeWidth={3} + style={styles.searchBtnIcon} + /> </TouchableOpacity> </View> ) @@ -59,33 +70,28 @@ const styles = StyleSheet.create({ titleContainer: { flexDirection: 'row', alignItems: 'baseline', - marginLeft: 'auto', marginRight: 'auto', }, title: { - fontSize: 16, + fontSize: 21, fontWeight: '600', }, subtitle: { - fontSize: 15, - marginLeft: 3, + fontSize: 18, + marginLeft: 6, color: colors.gray4, maxWidth: 200, }, - cornerPlaceholder: { - width: 30, - height: 30, - }, backIcon: {width: 30, height: 30}, - searchBtn: { + btn: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', backgroundColor: colors.gray1, - width: 30, - height: 30, - borderRadius: 15, + width: 36, + height: 36, + borderRadius: 20, }, searchBtnIcon: { color: colors.black, diff --git a/src/view/lib/icons.tsx b/src/view/lib/icons.tsx index 05b1ec601..7e3313597 100644 --- a/src/view/lib/icons.tsx +++ b/src/view/lib/icons.tsx @@ -94,15 +94,17 @@ export function HomeIconSolid({ export function MagnifyingGlassIcon({ style, size, + strokeWidth = 2, }: { style?: StyleProp<ViewStyle> size?: string | number + strokeWidth?: number }) { return ( <Svg fill="none" viewBox="0 0 24 24" - strokeWidth={2} + strokeWidth={strokeWidth} stroke="currentColor" width={size || 24} height={size || 24} diff --git a/src/view/screens/Home.tsx b/src/view/screens/Home.tsx index 8dd7ca411..5925b6f80 100644 --- a/src/view/screens/Home.tsx +++ b/src/view/screens/Home.tsx @@ -47,6 +47,7 @@ export const Home = observer(function Home({ if (!visible) { return } + if (hasSetup) { console.log('Updating home feed') defaultFeedView.update() @@ -80,7 +81,11 @@ export const Home = observer(function Home({ return ( <View style={s.flex1}> - <ViewHeader title="Bluesky" subtitle="Private Beta" /> + <ViewHeader + title="Bluesky" + subtitle="Private Beta" + onPost={onCreatePost} + /> <Feed key="default" feed={defaultFeedView} @@ -106,8 +111,8 @@ const styles = StyleSheet.create({ left: 10, bottom: 15, backgroundColor: colors.pink3, - paddingHorizontal: 10, - paddingVertical: 8, + paddingHorizontal: 12, + paddingVertical: 10, borderRadius: 30, shadowColor: '#000', shadowOpacity: 0.3, @@ -117,5 +122,6 @@ const styles = StyleSheet.create({ color: colors.white, fontWeight: 'bold', marginLeft: 5, + fontSize: 16, }, }) diff --git a/src/view/screens/Profile.tsx b/src/view/screens/Profile.tsx index d1abcd731..2cfcf975c 100644 --- a/src/view/screens/Profile.tsx +++ b/src/view/screens/Profile.tsx @@ -15,7 +15,8 @@ import {PostFeedLoadingPlaceholder} from '../com/util/LoadingPlaceholder' import {ErrorScreen} from '../com/util/ErrorScreen' import {ErrorMessage} from '../com/util/ErrorMessage' import {EmptyState} from '../com/util/EmptyState' -import Toast from '../com/util/Toast' +import {ViewHeader} from '../com/util/ViewHeader' +import * as Toast from '../com/util/Toast' import {s, colors} from '../lib/styles' const LOADING_ITEM = {_reactKey: '__loading__'} @@ -77,10 +78,7 @@ export const Profile = observer(({navIdx, visible, params}: ScreenParams) => { `You'll be able to invite them again if you change your mind.`, async () => { await uiState.members.removeMember(membership.did) - Toast.show(`User removed`, { - duration: Toast.durations.LONG, - position: Toast.positions.TOP, - }) + Toast.show(`User removed`) }, ), ) @@ -219,8 +217,11 @@ export const Profile = observer(({navIdx, visible, params}: ScreenParams) => { renderItem = () => <View /> } + const title = + uiState.profile.displayName || uiState.profile.handle || params.name return ( <View style={styles.container}> + <ViewHeader title={title} /> {uiState.profile.hasError ? ( <ErrorScreen title="Failed to load profile" diff --git a/src/view/shell/mobile/MainMenu.tsx b/src/view/shell/mobile/MainMenu.tsx index d05e70a81..8a7264612 100644 --- a/src/view/shell/mobile/MainMenu.tsx +++ b/src/view/shell/mobile/MainMenu.tsx @@ -8,7 +8,6 @@ import { TouchableWithoutFeedback, View, } from 'react-native' -import {useSafeAreaInsets} from 'react-native-safe-area-context' import Animated, { useSharedValue, useAnimatedStyle, @@ -25,10 +24,17 @@ import {CreateSceneModel} from '../../../state/models/shell-ui' import {s, colors} from '../../lib/styles' export const MainMenu = observer( - ({active, onClose}: {active: boolean; onClose: () => void}) => { + ({ + active, + insetBottom, + onClose, + }: { + active: boolean + insetBottom: number + onClose: () => void + }) => { const store = useStores() const initInterp = useSharedValue<number>(0) - const insets = useSafeAreaInsets() useEffect(() => { if (active) { @@ -172,7 +178,7 @@ export const MainMenu = observer( <Animated.View style={[ styles.wrapper, - {bottom: insets.bottom + 55}, + {bottom: insetBottom + 45}, wrapperAnimStyle, ]}> <SafeAreaView> @@ -267,7 +273,8 @@ const styles = StyleSheet.create({ alignItems: 'center', height: 40, paddingHorizontal: 10, - marginBottom: 16, + marginTop: 12, + marginBottom: 20, }, section: { paddingHorizontal: 10, diff --git a/src/view/shell/mobile/index.tsx b/src/view/shell/mobile/index.tsx index e7c695ca9..ccde52a2c 100644 --- a/src/view/shell/mobile/index.tsx +++ b/src/view/shell/mobile/index.tsx @@ -70,7 +70,7 @@ const Btn = ({ onPress?: (event: GestureResponderEvent) => void onLongPress?: (event: GestureResponderEvent) => void }) => { - let size = 21 + let size = 24 let addedStyles let IconEl if (icon === 'menu') { @@ -79,17 +79,17 @@ const Btn = ({ IconEl = GridIconSolid } else if (icon === 'home') { IconEl = HomeIcon - size = 24 + size = 27 } else if (icon === 'home-solid') { IconEl = HomeIconSolid - size = 24 + size = 27 } else if (icon === 'bell') { IconEl = BellIcon - size = 24 + size = 27 addedStyles = {position: 'relative', top: -1} as ViewStyle } else if (icon === 'bell-solid') { IconEl = BellIconSolid - size = 24 + size = 27 addedStyles = {position: 'relative', top: -1} as ViewStyle } else { IconEl = FontAwesomeIcon @@ -316,7 +316,7 @@ export const MobileShell: React.FC = observer(() => { <View style={[ styles.bottomBar, - {paddingBottom: clamp(safeAreaInsets.bottom, 15, 30)}, + {paddingBottom: clamp(safeAreaInsets.bottom, 15, 40)}, ]}> <Btn icon={isAtHome ? 'home-solid' : 'home'} @@ -343,6 +343,7 @@ export const MobileShell: React.FC = observer(() => { </View> <MainMenu active={isMainMenuActive} + insetBottom={clamp(safeAreaInsets.bottom, 15, 40)} onClose={() => setMainMenuActive(false)} /> <Modal /> @@ -491,7 +492,7 @@ const styles = StyleSheet.create({ }, ctrl: { flex: 1, - paddingTop: 15, + paddingTop: 12, paddingBottom: 5, }, notificationCount: { diff --git a/yarn.lock b/yarn.lock index 76c7f64f5..5209a89db 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11718,6 +11718,11 @@ thunky@^1.0.2: resolved "https://registry.yarnpkg.com/thunky/-/thunky-1.1.0.tgz#5abaf714a9405db0504732bbccd2cedd9ef9537d" integrity sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA== +tlds@^1.234.0: + version "1.234.0" + resolved "https://registry.yarnpkg.com/tlds/-/tlds-1.234.0.tgz#f61fe73f6e85c51f8503181f47dcfbd18c6910db" + integrity sha512-TNDfeyDIC+oroH44bMbWC+Jn/2qNrfRvDK2EXt1icOXYG5NMqoRyUosADrukfb4D8lJ3S1waaBWSvQro0erdng== + tmpl@1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.5.tgz#8683e0b902bb9c20c4f726e3c0b69f36518c07cc" |