diff --git a/__tests__/string-utils.ts b/__tests__/string-utils.ts index c677b44d..fc7a8f27 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 35e4615a..13af3997 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 06da5e4e..cc6f3815 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( diff --git a/src/lib/strings.ts b/src/lib/strings.ts index 032eec56..fb9d15b2 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, @@ -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]+)|((?[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)(?[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 92c394df..33db426a 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 | undefined _loadMorePromise: Promise | undefined _loadLatestPromise: Promise | 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 09189cfb..80e5c80c 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 10305adb..ce42ee17 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(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({ - Cancel + Cancel {isProcessing ? ( @@ -156,7 +165,9 @@ export const ComposePost = observer(function ComposePost({ start={{x: 0, y: 0}} end={{x: 1, y: 1}} style={styles.postBtn}> - Post + + {replyTo ? 'Reply' : 'Post'} + ) : ( @@ -178,39 +189,46 @@ export const ComposePost = observer(function ComposePost({ )} {replyTo ? ( - - - Replying to{' '} + + + - - - {replyTo.text} + + {replyTo.text} + ) : undefined} - onChangeText(text)} - placeholder={ - replyTo - ? 'Write your reply' - : photoUris.length === 0 - ? "What's up?" - : 'Add a comment...' - } - style={styles.textInput}> - {textDecorated} - + + + onChangeText(text)} + placeholder={replyTo ? 'Write your reply' : "What's up?"} + style={styles.textInput}> + {textDecorated} + + {photoUris.length !== 0 && ( {photoUris.length !== 0 && - photoUris.map(item => ( + photoUris.map((item, index) => ( - {localPhotos.photos.map(item => ( + {localPhotos.photos.map((item, index) => ( { 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 f9fd7e7d..7805e00d 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 ( - - - - + + {!noAvi ? ( + + + + ) : undefined} - What's up? + {text} - Post + {btn} ) @@ -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 d8cb0c4d..d5875f0f 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 44537462..646d5b24 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 f739b084..50acccf6 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 2d4e372c..8df38daf 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 7d735a66..72bc0676 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 95b02837..85c241ce 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 ( - - - - - - - - - - - - {item.author.displayName || item.author.handle} - - - - · {ago(item.indexedAt)} - - - - + + + + + - - - - - - @{item.author.handle} - - - - - - - - - {item._isHighlightedPost && hasEngagement ? ( - - {item.repostCount ? ( + + - - - {item.repostCount} - {' '} - {pluralize(item.repostCount, 'repost')} + style={styles.metaItem} + href={authorHref} + title={authorTitle}> + + {item.author.displayName || item.author.handle} - ) : ( - <> - )} - {item.upvoteCount ? ( + + · {ago(item.indexedAt)} + + + + + + + - - - {item.upvoteCount} - {' '} - {pluralize(item.upvoteCount, 'upvote')} + style={styles.metaItem} + href={authorHref} + title={authorTitle}> + + @{item.author.handle} - ) : ( - <> - )} + + + + + + + + + {item._isHighlightedPost && hasEngagement ? ( + + {item.repostCount ? ( + + + + {item.repostCount} + {' '} + {pluralize(item.repostCount, 'repost')} + + + ) : ( + <> + )} + {item.upvoteCount ? ( + + + + {item.upvoteCount} + {' '} + {pluralize(item.upvoteCount, 'upvote')} + + + ) : ( + <> + )} + + ) : ( + <> + )} + + - ) : ( - <> - )} - - - + + ) } 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 d0df1b29..4d668cac 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 4063b200..4d50531b 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 9325a88a..1b25c7c1 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 ( - {store.nav.tab.canGoBack ? ( - - - - ) : undefined} - - - - {store.nav.tab.canGoBack ? ( - - - - ) : undefined} - - - void @@ -30,17 +31,17 @@ export function PostCtrls(opts: PostCtrlsOpts) { const interp2 = useSharedValue(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) { - {opts.replyCount} + {typeof opts.replyCount !== 'undefined' ? ( + {opts.replyCount} + ) : undefined} @@ -77,17 +80,19 @@ export function PostCtrls(opts: PostCtrlsOpts) { opts.isReposted ? styles.ctrlIconReposted : styles.ctrlIcon } icon="retweet" - size={18} + size={opts.big ? 22 : 18} /> - - {opts.repostCount} - + {typeof opts.repostCount !== 'undefined' ? ( + + {opts.repostCount} + + ) : undefined} @@ -96,19 +101,28 @@ export function PostCtrls(opts: PostCtrlsOpts) { onPress={onPressToggleUpvoteWrapper}> {opts.isUpvoted ? ( - + ) : ( - + )} - - {opts.upvoteCount} - + {typeof opts.upvoteCount !== 'undefined' ? ( + + {opts.upvoteCount} + + ) : undefined} diff --git a/src/view/com/util/Toast.native.tsx b/src/view/com/util/Toast.native.tsx deleted file mode 100644 index 4b9fd7f8..00000000 --- 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 1726b71b..197f4742 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 Toast from 'react-native-root-toast' -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 = ({}) => { - const [activeToast, setActiveToast] = useState() - useEffect(() => { - globalSetActiveToast = (t: ActiveToast | undefined) => { - setActiveToast(t) - } +export function show(message: string) { + Toast.show(message, { + duration: Toast.durations.LONG, + position: 50, + shadow: true, + animation: true, + hideOnPress: true, }) - return ( - <> - {activeToast && ( - - - {activeToast.text} - - )} - - ) -} - -// 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 55a71ea2..50b7e653 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({ - ) : ( - - )} + ) : undefined} {title} {subtitle ? ( @@ -37,8 +39,17 @@ export function ViewHeader({ ) : undefined} - - + + + + + ) @@ -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 05b1ec60..7e331359 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 size?: string | number + strokeWidth?: number }) { return ( - + { `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 = () => } + const title = + uiState.profile.displayName || uiState.profile.handle || params.name return ( + {uiState.profile.hasError ? ( void}) => { + ({ + active, + insetBottom, + onClose, + }: { + active: boolean + insetBottom: number + onClose: () => void + }) => { const store = useStores() const initInterp = useSharedValue(0) - const insets = useSafeAreaInsets() useEffect(() => { if (active) { @@ -172,7 +178,7 @@ export const MainMenu = observer( @@ -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 e7c695ca..ccde52a2 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(() => { { setMainMenuActive(false)} /> @@ -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 76c7f64f..5209a89d 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"