diff --git a/package.json b/package.json
index 23d7149de..57a6d02ac 100644
--- a/package.json
+++ b/package.json
@@ -84,10 +84,12 @@
"lodash.isequal": "^4.5.0",
"lodash.omit": "^4.5.0",
"lodash.samplesize": "^4.2.0",
+ "lodash.set": "^4.3.2",
"lodash.shuffle": "^4.2.0",
"lru_map": "^0.4.1",
"mobx": "^6.6.1",
"mobx-react-lite": "^3.4.0",
+ "mobx-utils": "^6.0.6",
"normalize-url": "^8.0.0",
"patch-package": "^6.5.1",
"postinstall-postinstall": "^2.1.0",
@@ -143,6 +145,7 @@
"@types/lodash.isequal": "^4.5.6",
"@types/lodash.omit": "^4.5.7",
"@types/lodash.samplesize": "^4.2.7",
+ "@types/lodash.set": "^4.3.7",
"@types/lodash.shuffle": "^4.2.7",
"@types/react-avatar-editor": "^13.0.0",
"@types/react-native": "^0.67.3",
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>
diff --git a/yarn.lock b/yarn.lock
index 8685a0d4e..f1cb70cf8 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -4540,6 +4540,13 @@
dependencies:
"@types/lodash" "*"
+"@types/lodash.set@^4.3.7":
+ version "4.3.7"
+ resolved "https://registry.yarnpkg.com/@types/lodash.set/-/lodash.set-4.3.7.tgz#784fccea3fbef4d0949d1897a780f592da700942"
+ integrity sha512-bS5Wkg/nrT82YUfkNYPSccFrNZRL+irl7Yt4iM6OTSQ0VZJED2oUIVm15NkNtUAQ8SRhCe+axqERUV6MJgkeEg==
+ dependencies:
+ "@types/lodash" "*"
+
"@types/lodash.shuffle@^4.2.7":
version "4.2.7"
resolved "https://registry.yarnpkg.com/@types/lodash.shuffle/-/lodash.shuffle-4.2.7.tgz#b714d829af948a266b0df1477d629c70de2f4c72"
@@ -11767,6 +11774,11 @@ lodash.samplesize@^4.2.0:
resolved "https://registry.yarnpkg.com/lodash.samplesize/-/lodash.samplesize-4.2.0.tgz#460762fbb2b342290517499e90d51586db465ff9"
integrity sha512-1ZhKV7/nuISuaQdxfCqrs4HHxXIYN+0Z4f7NMQn2PHkxFZJGavJQ1j/paxyJnLJmN2ZamNN6SMepneV+dCgQTA==
+lodash.set@^4.3.2:
+ version "4.3.2"
+ resolved "https://registry.yarnpkg.com/lodash.set/-/lodash.set-4.3.2.tgz#d8757b1da807dde24816b0d6a84bea1a76230b23"
+ integrity sha512-4hNPN5jlm/N/HLMCO43v8BXKq9Z7QdAGc/VGrRD61w8gN9g/6jF9A4L1pbUgBLCffi0w9VsXfTOij5x8iTyFvg==
+
lodash.shuffle@^4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/lodash.shuffle/-/lodash.shuffle-4.2.0.tgz#145b5053cf875f6f5c2a33f48b6e9948c6ec7b4b"
@@ -12514,6 +12526,11 @@ mobx-react-lite@^3.4.0:
resolved "https://registry.yarnpkg.com/mobx-react-lite/-/mobx-react-lite-3.4.3.tgz#3a4c22c30bfaa8b1b2aa48d12b2ba811c0947ab7"
integrity sha512-NkJREyFTSUXR772Qaai51BnE1voWx56LOL80xG7qkZr6vo8vEaLF3sz1JNUVh+rxmUzxYaqOhfuxTfqUh0FXUg==
+mobx-utils@^6.0.6:
+ version "6.0.6"
+ resolved "https://registry.yarnpkg.com/mobx-utils/-/mobx-utils-6.0.6.tgz#99a2e0d54e958e4c9de4b35729e0c3768b6afc43"
+ integrity sha512-lzJtxOWgj3Dp2HeXviInV3ZRY4YhThzRHXuy90oKXDH2g+ymJGIts4bdjb7NQuSi34V25cMZoQX7TkHJQuKLOQ==
+
mobx@^6.6.1:
version "6.8.0"
resolved "https://registry.yarnpkg.com/mobx/-/mobx-6.8.0.tgz#59051755fdb5c8a9f3f2e0a9b6abaf86bab7f843"
|