diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/lib/async/revertible.ts | 52 | ||||
-rw-r--r-- | src/state/models/content/post-thread.ts | 74 | ||||
-rw-r--r-- | src/state/models/feeds/posts.ts | 74 | ||||
-rw-r--r-- | src/view/com/util/PostCtrls.tsx | 74 |
4 files changed, 151 insertions, 123 deletions
diff --git a/src/lib/async/revertible.ts b/src/lib/async/revertible.ts new file mode 100644 index 000000000..3c8e3e8f9 --- /dev/null +++ b/src/lib/async/revertible.ts @@ -0,0 +1,52 @@ +import {runInAction} from 'mobx' +import {deepObserve} from 'mobx-utils' +import set from 'lodash.set' + +const ongoingActions = new Set<any>() + +export const updateDataOptimistically = async < + T extends Record<string, any>, + U, +>( + model: T, + preUpdate: () => void, + serverUpdate: () => Promise<U>, + postUpdate?: (res: U) => void, +): Promise<void> => { + if (ongoingActions.has(model)) { + return + } + ongoingActions.add(model) + + const prevState: Map<string, any> = new Map<string, any>() + const dispose = deepObserve(model, (change, path) => { + if (change.observableKind === 'object') { + if (change.type === 'update') { + prevState.set( + [path, change.name].filter(Boolean).join('.'), + change.oldValue, + ) + } else if (change.type === 'add') { + prevState.set([path, change.name].filter(Boolean).join('.'), undefined) + } + } + }) + preUpdate() + dispose() + + try { + const res = await serverUpdate() + runInAction(() => { + postUpdate?.(res) + }) + } catch (error) { + runInAction(() => { + prevState.forEach((value, path) => { + set(model, path, value) + }) + }) + throw error + } finally { + ongoingActions.delete(model) + } +} diff --git a/src/state/models/content/post-thread.ts b/src/state/models/content/post-thread.ts index d3e773673..794beae20 100644 --- a/src/state/models/content/post-thread.ts +++ b/src/state/models/content/post-thread.ts @@ -9,6 +9,7 @@ import {AtUri} from '@atproto/api' import {RootStoreModel} from '../root-store' import * as apilib from 'lib/api/index' import {cleanError} from 'lib/strings/errors' +import {updateDataOptimistically} from 'lib/async/revertible' function* reactKeyGenerator(): Generator<string> { let counter = 0 @@ -134,45 +135,56 @@ export class PostThreadItemModel { } async toggleLike() { - if (this.post.viewer?.like) { - await this.rootStore.agent.deleteLike(this.post.viewer.like) - runInAction(() => { - this.post.likeCount = this.post.likeCount || 0 - this.post.viewer = this.post.viewer || {} - this.post.likeCount-- - this.post.viewer.like = undefined - }) + this.post.viewer = this.post.viewer || {} + if (this.post.viewer.like) { + const url = this.post.viewer.like + await updateDataOptimistically( + this.post, + () => { + this.post.likeCount = (this.post.likeCount || 0) - 1 + this.post.viewer!.like = undefined + }, + () => this.rootStore.agent.deleteLike(url), + ) } else { - const res = await this.rootStore.agent.like(this.post.uri, this.post.cid) - runInAction(() => { - this.post.likeCount = this.post.likeCount || 0 - this.post.viewer = this.post.viewer || {} - this.post.likeCount++ - this.post.viewer.like = res.uri - }) + await updateDataOptimistically( + this.post, + () => { + this.post.likeCount = (this.post.likeCount || 0) + 1 + this.post.viewer!.like = 'pending' + }, + () => this.rootStore.agent.like(this.post.uri, this.post.cid), + res => { + this.post.viewer!.like = res.uri + }, + ) } } async toggleRepost() { + this.post.viewer = this.post.viewer || {} if (this.post.viewer?.repost) { - await this.rootStore.agent.deleteRepost(this.post.viewer.repost) - runInAction(() => { - this.post.repostCount = this.post.repostCount || 0 - this.post.viewer = this.post.viewer || {} - this.post.repostCount-- - this.post.viewer.repost = undefined - }) + const url = this.post.viewer.repost + await updateDataOptimistically( + this.post, + () => { + this.post.repostCount = (this.post.repostCount || 0) - 1 + this.post.viewer!.repost = undefined + }, + () => this.rootStore.agent.deleteRepost(url), + ) } else { - const res = await this.rootStore.agent.repost( - this.post.uri, - this.post.cid, + await updateDataOptimistically( + this.post, + () => { + this.post.repostCount = (this.post.repostCount || 0) + 1 + this.post.viewer!.repost = 'pending' + }, + () => this.rootStore.agent.repost(this.post.uri, this.post.cid), + res => { + this.post.viewer!.repost = res.uri + }, ) - runInAction(() => { - this.post.repostCount = this.post.repostCount || 0 - this.post.viewer = this.post.viewer || {} - this.post.repostCount++ - this.post.viewer.repost = res.uri - }) } } diff --git a/src/state/models/feeds/posts.ts b/src/state/models/feeds/posts.ts index c82453946..e3328c71a 100644 --- a/src/state/models/feeds/posts.ts +++ b/src/state/models/feeds/posts.ts @@ -19,6 +19,7 @@ import { mergePosts, } from 'lib/api/build-suggested-posts' import {FeedTuner, FeedViewPostsSlice} from 'lib/api/feed-manip' +import {updateDataOptimistically} from 'lib/async/revertible' type FeedViewPost = AppBskyFeedDefs.FeedViewPost type ReasonRepost = AppBskyFeedDefs.ReasonRepost @@ -91,45 +92,56 @@ export class PostsFeedItemModel { } async toggleLike() { - if (this.post.viewer?.like) { - await this.rootStore.agent.deleteLike(this.post.viewer.like) - runInAction(() => { - this.post.likeCount = this.post.likeCount || 0 - this.post.viewer = this.post.viewer || {} - this.post.likeCount-- - this.post.viewer.like = undefined - }) + this.post.viewer = this.post.viewer || {} + if (this.post.viewer.like) { + const url = this.post.viewer.like + await updateDataOptimistically( + this.post, + () => { + this.post.likeCount = (this.post.likeCount || 0) - 1 + this.post.viewer!.like = undefined + }, + () => this.rootStore.agent.deleteLike(url), + ) } else { - const res = await this.rootStore.agent.like(this.post.uri, this.post.cid) - runInAction(() => { - this.post.likeCount = this.post.likeCount || 0 - this.post.viewer = this.post.viewer || {} - this.post.likeCount++ - this.post.viewer.like = res.uri - }) + await updateDataOptimistically( + this.post, + () => { + this.post.likeCount = (this.post.likeCount || 0) + 1 + this.post.viewer!.like = 'pending' + }, + () => this.rootStore.agent.like(this.post.uri, this.post.cid), + res => { + this.post.viewer!.like = res.uri + }, + ) } } async toggleRepost() { + this.post.viewer = this.post.viewer || {} if (this.post.viewer?.repost) { - await this.rootStore.agent.deleteRepost(this.post.viewer.repost) - runInAction(() => { - this.post.repostCount = this.post.repostCount || 0 - this.post.viewer = this.post.viewer || {} - this.post.repostCount-- - this.post.viewer.repost = undefined - }) + const url = this.post.viewer.repost + await updateDataOptimistically( + this.post, + () => { + this.post.repostCount = (this.post.repostCount || 0) - 1 + this.post.viewer!.repost = undefined + }, + () => this.rootStore.agent.deleteRepost(url), + ) } else { - const res = await this.rootStore.agent.repost( - this.post.uri, - this.post.cid, + await updateDataOptimistically( + this.post, + () => { + this.post.repostCount = (this.post.repostCount || 0) + 1 + this.post.viewer!.repost = 'pending' + }, + () => this.rootStore.agent.repost(this.post.uri, this.post.cid), + res => { + this.post.viewer!.repost = res.uri + }, ) - runInAction(() => { - this.post.repostCount = this.post.repostCount || 0 - this.post.viewer = this.post.viewer || {} - this.post.repostCount++ - this.post.viewer.repost = res.uri - }) } } diff --git a/src/view/com/util/PostCtrls.tsx b/src/view/com/util/PostCtrls.tsx index 4497e7058..6441d3c77 100644 --- a/src/view/com/util/PostCtrls.tsx +++ b/src/view/com/util/PostCtrls.tsx @@ -103,8 +103,6 @@ export function PostCtrls(opts: PostCtrlsOpts) { }), [theme], ) as StyleProp<ViewStyle> - const [repostMod, setRepostMod] = React.useState<number>(0) - const [likeMod, setLikeMod] = React.useState<number>(0) // DISABLED see #135 // const repostRef = React.useRef<TriggerableAnimatedRef | null>(null) // const likeRef = React.useRef<TriggerableAnimatedRef | null>(null) @@ -112,11 +110,7 @@ export function PostCtrls(opts: PostCtrlsOpts) { store.shell.closeModal() if (!opts.isReposted) { ReactNativeHapticFeedback.trigger('impactMedium') - setRepostMod(1) - opts - .onPressToggleRepost() - .catch(_e => undefined) - .then(() => setRepostMod(0)) + opts.onPressToggleRepost().catch(_e => undefined) // DISABLED see #135 // repostRef.current?.trigger( // {start: ctrlAnimStart, style: ctrlAnimStyle}, @@ -126,11 +120,7 @@ export function PostCtrls(opts: PostCtrlsOpts) { // }, // ) } else { - setRepostMod(-1) - opts - .onPressToggleRepost() - .catch(_e => undefined) - .then(() => setRepostMod(0)) + opts.onPressToggleRepost().catch(_e => undefined) } } @@ -157,14 +147,10 @@ export function PostCtrls(opts: PostCtrlsOpts) { }) } - const onPressToggleLikeWrapper = () => { + const onPressToggleLikeWrapper = async () => { if (!opts.isLiked) { ReactNativeHapticFeedback.trigger('impactMedium') - setLikeMod(1) - opts - .onPressToggleLike() - .catch(_e => undefined) - .then(() => setLikeMod(0)) + await opts.onPressToggleLike().catch(_e => undefined) // DISABLED see #135 // likeRef.current?.trigger( // {start: ctrlAnimStart, style: ctrlAnimStyle}, @@ -173,12 +159,10 @@ export function PostCtrls(opts: PostCtrlsOpts) { // setLikeMod(0) // }, // ) + // setIsLikedPressed(false) } else { - setLikeMod(-1) - opts - .onPressToggleLike() - .catch(_e => undefined) - .then(() => setLikeMod(0)) + await opts.onPressToggleLike().catch(_e => undefined) + // setIsLikedPressed(false) } } @@ -210,35 +194,22 @@ export function PostCtrls(opts: PostCtrlsOpts) { style={styles.ctrl}> <RepostIcon style={ - opts.isReposted || repostMod > 0 + opts.isReposted ? (styles.ctrlIconReposted as StyleProp<ViewStyle>) : defaultCtrlColor } strokeWidth={2.4} size={opts.big ? 24 : 20} /> - { - undefined /*DISABLED see #135 <TriggerableAnimated ref={repostRef}> - <RepostIcon - style={ - (opts.isReposted - ? styles.ctrlIconReposted - : defaultCtrlColor) as ViewStyle - } - strokeWidth={2.4} - size={opts.big ? 24 : 20} - /> - </TriggerableAnimated>*/ - } {typeof opts.repostCount !== 'undefined' ? ( <Text testID="repostCount" style={ - opts.isReposted || repostMod > 0 + opts.isReposted ? [s.bold, s.green3, s.f15, s.ml5] : [defaultCtrlColor, s.f15, s.ml5] }> - {opts.repostCount + repostMod} + {opts.repostCount} </Text> ) : undefined} </TouchableOpacity> @@ -249,7 +220,7 @@ export function PostCtrls(opts: PostCtrlsOpts) { style={styles.ctrl} hitSlop={HITSLOP} onPress={onPressToggleLikeWrapper}> - {opts.isLiked || likeMod > 0 ? ( + {opts.isLiked ? ( <HeartIconSolid style={styles.ctrlIconLiked as StyleProp<ViewStyle>} size={opts.big ? 22 : 16} @@ -261,34 +232,15 @@ export function PostCtrls(opts: PostCtrlsOpts) { size={opts.big ? 20 : 16} /> )} - { - undefined /*DISABLED see #135 <TriggerableAnimated ref={likeRef}> - {opts.isLiked || likeMod > 0 ? ( - <HeartIconSolid - style={styles.ctrlIconLiked as ViewStyle} - size={opts.big ? 22 : 16} - /> - ) : ( - <HeartIcon - style={[ - defaultCtrlColor as ViewStyle, - opts.big ? styles.mt1 : undefined, - ]} - strokeWidth={3} - size={opts.big ? 20 : 16} - /> - )} - </TriggerableAnimated>*/ - } {typeof opts.likeCount !== 'undefined' ? ( <Text testID="likeCount" style={ - opts.isLiked || likeMod > 0 + opts.isLiked ? [s.bold, s.red3, s.f15, s.ml5] : [defaultCtrlColor, s.f15, s.ml5] }> - {opts.likeCount + likeMod} + {opts.likeCount} </Text> ) : undefined} </TouchableOpacity> |