Implement like and repost

This commit is contained in:
Paul Frazee 2022-07-22 11:18:47 -05:00
parent cc8a170204
commit 0ec0ba996f
12 changed files with 307 additions and 40 deletions

View file

@ -4,7 +4,16 @@
*/
// import {ReactNativeStore} from './auth'
import {AdxClient, AdxRepoClient, AdxUri, bsky} from '@adxp/mock-api'
import {
AdxClient,
AdxRepoClient,
AdxRepoCollectionClient,
AdxUri,
bsky,
SchemaOpt,
ListRecordsResponseValidated,
GetRecordResponseValidated,
} from '@adxp/mock-api'
import * as storage from './storage'
import {postTexts} from './mock-data/post-texts'
import {replyTexts} from './mock-data/reply-texts'
@ -19,6 +28,78 @@ export async function setup(adx: AdxClient) {
)
}
export async function like(adx: AdxClient, user: string, uri: string) {
await adx.repo(user, true).collection('blueskyweb.xyz:Likes').create('Like', {
$type: 'blueskyweb.xyz:Like',
subject: uri,
createdAt: new Date().toISOString(),
})
}
export async function unlike(adx: AdxClient, user: string, uri: string) {
const coll = adx.repo(user, true).collection('blueskyweb.xyz:Likes')
const numDels = await deleteWhere(coll, 'Like', record => {
return record.value.subject === uri
})
return numDels > 0
}
export async function repost(adx: AdxClient, user: string, uri: string) {
await adx
.repo(user, true)
.collection('blueskyweb.xyz:Posts')
.create('Repost', {
$type: 'blueskyweb.xyz:Repost',
subject: uri,
createdAt: new Date().toISOString(),
})
}
export async function unrepost(adx: AdxClient, user: string, uri: string) {
const coll = adx.repo(user, true).collection('blueskyweb.xyz:Posts')
const numDels = await deleteWhere(coll, 'Repost', record => {
return record.value.subject === uri
})
return numDels > 0
}
type WherePred = (record: GetRecordResponseValidated) => Boolean
async function deleteWhere(
coll: AdxRepoCollectionClient,
schema: SchemaOpt,
cond: WherePred,
) {
const toDelete: string[] = []
iterateAll(coll, schema, record => {
if (cond(record)) {
toDelete.push(record.key)
}
})
for (const key of toDelete) {
await coll.del(key)
}
return toDelete.length
}
type IterateAllCb = (record: GetRecordResponseValidated) => void
async function iterateAll(
coll: AdxRepoCollectionClient,
schema: SchemaOpt,
cb: IterateAllCb,
) {
let cursor
let res: ListRecordsResponseValidated
do {
res = await coll.list(schema, {after: cursor, limit: 100})
for (const record of res.records) {
if (record.valid) {
cb(record)
cursor = record.key
}
}
} while (res.records.length === 100)
}
// TEMPORARY
// mock api config
// =======

View file

@ -1,6 +1,17 @@
import {makeAutoObservable} from 'mobx'
import {makeAutoObservable, runInAction} from 'mobx'
import {bsky} from '@adxp/mock-api'
import _omit from 'lodash.omit'
import {RootStoreModel} from './root-store'
import * as apilib from '../lib/api'
export class FeedViewItemMyStateModel {
hasLiked: boolean = false
hasReposted: boolean = false
constructor() {
makeAutoObservable(this)
}
}
export class FeedViewItemModel implements bsky.FeedView.FeedItem {
// ui state
@ -19,11 +30,51 @@ export class FeedViewItemModel implements bsky.FeedView.FeedItem {
repostCount: number = 0
likeCount: number = 0
indexedAt: string = ''
myState = new FeedViewItemMyStateModel()
constructor(reactKey: string, v: bsky.FeedView.FeedItem) {
makeAutoObservable(this)
constructor(
public rootStore: RootStoreModel,
reactKey: string,
v: bsky.FeedView.FeedItem,
) {
makeAutoObservable(this, {rootStore: false})
this._reactKey = reactKey
Object.assign(this, v)
Object.assign(this, _omit(v, 'myState'))
if (v.myState) {
Object.assign(this.myState, v.myState)
}
}
async toggleLike() {
if (this.myState.hasLiked) {
await apilib.unlike(this.rootStore.api, 'alice.com', this.uri)
runInAction(() => {
this.likeCount--
this.myState.hasLiked = false
})
} else {
await apilib.like(this.rootStore.api, 'alice.com', this.uri)
runInAction(() => {
this.likeCount++
this.myState.hasLiked = true
})
}
}
async toggleRepost() {
if (this.myState.hasReposted) {
await apilib.unrepost(this.rootStore.api, 'alice.com', this.uri)
runInAction(() => {
this.repostCount--
this.myState.hasReposted = false
})
} else {
await apilib.repost(this.rootStore.api, 'alice.com', this.uri)
runInAction(() => {
this.repostCount++
this.myState.hasReposted = true
})
}
}
}
@ -177,6 +228,6 @@ export class FeedViewModel implements bsky.FeedView.Response {
private _append(keyId: number, item: bsky.FeedView.FeedItem) {
// TODO: validate .record
this.feed.push(new FeedViewItemModel(`item-${keyId}`, item))
this.feed.push(new FeedViewItemModel(this.rootStore, `item-${keyId}`, item))
}
}

View file

@ -2,6 +2,7 @@ import {makeAutoObservable, runInAction} from 'mobx'
import {bsky, AdxUri} from '@adxp/mock-api'
import _omit from 'lodash.omit'
import {RootStoreModel} from './root-store'
import * as apilib from '../lib/api'
function* reactKeyGenerator(): Generator<string> {
let counter = 0
@ -10,6 +11,15 @@ function* reactKeyGenerator(): Generator<string> {
}
}
export class PostThreadViewPostMyStateModel {
hasLiked: boolean = false
hasReposted: boolean = false
constructor() {
makeAutoObservable(this)
}
}
export class PostThreadViewPostModel implements bsky.PostThreadView.Post {
// ui state
_reactKey: string = ''
@ -30,12 +40,20 @@ export class PostThreadViewPostModel implements bsky.PostThreadView.Post {
repostCount: number = 0
likeCount: number = 0
indexedAt: string = ''
myState = new PostThreadViewPostMyStateModel()
constructor(reactKey: string, v?: bsky.PostThreadView.Post) {
makeAutoObservable(this)
constructor(
public rootStore: RootStoreModel,
reactKey: string,
v?: bsky.PostThreadView.Post,
) {
makeAutoObservable(this, {rootStore: false})
this._reactKey = reactKey
if (v) {
Object.assign(this, _omit(v, 'parent', 'replies')) // copy everything but the replies and the parent
Object.assign(this, _omit(v, 'parent', 'replies', 'myState')) // replies and parent are handled via assignTreeModels
if (v.myState) {
Object.assign(this.myState, v.myState)
}
}
}
@ -44,6 +62,7 @@ export class PostThreadViewPostModel implements bsky.PostThreadView.Post {
if (v.parent) {
// TODO: validate .record
const parentModel = new PostThreadViewPostModel(
this.rootStore,
keyGen.next().value,
v.parent,
)
@ -58,7 +77,11 @@ export class PostThreadViewPostModel implements bsky.PostThreadView.Post {
const replies = []
for (const item of v.replies) {
// TODO: validate .record
const itemModel = new PostThreadViewPostModel(keyGen.next().value, item)
const itemModel = new PostThreadViewPostModel(
this.rootStore,
keyGen.next().value,
item,
)
itemModel._depth = this._depth + 1
if (item.replies) {
itemModel.assignTreeModels(keyGen, item)
@ -68,10 +91,41 @@ export class PostThreadViewPostModel implements bsky.PostThreadView.Post {
this.replies = replies
}
}
}
const UNLOADED_THREAD = new PostThreadViewPostModel('')
export class PostThreadViewModel implements bsky.PostThreadView.Response {
async toggleLike() {
if (this.myState.hasLiked) {
await apilib.unlike(this.rootStore.api, 'alice.com', this.uri)
runInAction(() => {
this.likeCount--
this.myState.hasLiked = false
})
} else {
await apilib.like(this.rootStore.api, 'alice.com', this.uri)
runInAction(() => {
this.likeCount++
this.myState.hasLiked = true
})
}
}
async toggleRepost() {
if (this.myState.hasReposted) {
await apilib.unrepost(this.rootStore.api, 'alice.com', this.uri)
runInAction(() => {
this.repostCount--
this.myState.hasReposted = false
})
} else {
await apilib.repost(this.rootStore.api, 'alice.com', this.uri)
runInAction(() => {
this.repostCount++
this.myState.hasReposted = true
})
}
}
}
export class PostThreadViewModel {
// state
isLoading = false
isRefreshing = false
@ -81,7 +135,7 @@ export class PostThreadViewModel implements bsky.PostThreadView.Response {
params: bsky.PostThreadView.Params
// data
thread: PostThreadViewPostModel = UNLOADED_THREAD
thread?: PostThreadViewPostModel
constructor(
public rootStore: RootStoreModel,
@ -99,7 +153,7 @@ export class PostThreadViewModel implements bsky.PostThreadView.Response {
}
get hasContent() {
return this.thread !== UNLOADED_THREAD
return typeof this.thread !== 'undefined'
}
get hasError() {
@ -177,7 +231,11 @@ export class PostThreadViewModel implements bsky.PostThreadView.Response {
private _replaceAll(res: bsky.PostThreadView.Response) {
// TODO: validate .record
const keyGen = reactKeyGenerator()
const thread = new PostThreadViewPostModel(keyGen.next().value, res.thread)
const thread = new PostThreadViewPostModel(
this.rootStore,
keyGen.next().value,
res.thread,
)
thread._isHighlightedPost = true
thread.assignTreeModels(keyGen, res.thread)
this.thread = thread

View file

@ -2,6 +2,14 @@ import {makeAutoObservable} from 'mobx'
import {bsky} from '@adxp/mock-api'
import {RootStoreModel} from './root-store'
export class ProfileViewMyStateModel {
hasFollowed: boolean = false
constructor() {
makeAutoObservable(this)
}
}
export class ProfileViewModel implements bsky.ProfileView.Response {
// state
isLoading = false
@ -19,6 +27,7 @@ export class ProfileViewModel implements bsky.ProfileView.Response {
followsCount: number = 0
postsCount: number = 0
badges: bsky.ProfileView.Badge[] = []
myState = new ProfileViewMyStateModel()
constructor(
public rootStore: RootStoreModel,
@ -101,5 +110,8 @@ export class ProfileViewModel implements bsky.ProfileView.Response {
this.followsCount = res.followsCount
this.postsCount = res.postsCount
this.badges = res.badges
if (res.myState) {
Object.assign(this.myState, res.myState)
}
}
}