Finish the upvote/downvote implementation

zio/stable
Paul Frazee 2022-11-08 12:14:51 -06:00
parent e650d98924
commit 1fbc4cf1f2
32 changed files with 1207 additions and 587 deletions

View File

@ -50,21 +50,45 @@ export async function post(
) )
} }
export async function like(store: RootStoreModel, uri: string, cid: string) { export async function upvote(store: RootStoreModel, uri: string, cid: string) {
return await store.api.app.bsky.feed.like.create( return await store.api.app.bsky.feed.vote.create(
{did: store.me.did || ''}, {did: store.me.did || ''},
{ {
subject: {uri, cid}, subject: {uri, cid},
direction: 'up',
createdAt: new Date().toISOString(), createdAt: new Date().toISOString(),
}, },
) )
} }
export async function unlike(store: RootStoreModel, likeUri: string) { export async function unupvote(store: RootStoreModel, upvoteUri: string) {
const likeUrip = new AtUri(likeUri) const urip = new AtUri(upvoteUri)
return await store.api.app.bsky.feed.like.delete({ return await store.api.app.bsky.feed.vote.delete({
did: likeUrip.hostname, did: urip.hostname,
rkey: likeUrip.rkey, rkey: urip.rkey,
})
}
export async function downvote(
store: RootStoreModel,
uri: string,
cid: string,
) {
return await store.api.app.bsky.feed.vote.create(
{did: store.me.did || ''},
{
subject: {uri, cid},
direction: 'down',
createdAt: new Date().toISOString(),
},
)
}
export async function undownvote(store: RootStoreModel, downvoteUri: string) {
const urip = new AtUri(downvoteUri)
return await store.api.app.bsky.feed.vote.delete({
did: urip.hostname,
rkey: urip.rkey,
}) })
} }

View File

@ -6,7 +6,8 @@ import * as apilib from '../lib/api'
export class FeedItemMyStateModel { export class FeedItemMyStateModel {
repost?: string repost?: string
like?: string upvote?: string
downvote?: string
constructor() { constructor() {
makeAutoObservable(this) makeAutoObservable(this)
@ -29,7 +30,8 @@ export class FeedItemModel implements GetTimeline.FeedItem {
| GetTimeline.UnknownEmbed | GetTimeline.UnknownEmbed
replyCount: number = 0 replyCount: number = 0
repostCount: number = 0 repostCount: number = 0
likeCount: number = 0 upvoteCount: number = 0
downvoteCount: number = 0
indexedAt: string = '' indexedAt: string = ''
myState = new FeedItemMyStateModel() myState = new FeedItemMyStateModel()
@ -52,26 +54,53 @@ export class FeedItemModel implements GetTimeline.FeedItem {
this.embed = v.embed this.embed = v.embed
this.replyCount = v.replyCount this.replyCount = v.replyCount
this.repostCount = v.repostCount this.repostCount = v.repostCount
this.likeCount = v.likeCount this.upvoteCount = v.upvoteCount
this.downvoteCount = v.downvoteCount
this.indexedAt = v.indexedAt this.indexedAt = v.indexedAt
if (v.myState) { if (v.myState) {
this.myState.like = v.myState.like this.myState.upvote = v.myState.upvote
this.myState.downvote = v.myState.downvote
this.myState.repost = v.myState.repost this.myState.repost = v.myState.repost
} }
} }
async toggleLike() { async _clearVotes() {
if (this.myState.like) { if (this.myState.upvote) {
await apilib.unlike(this.rootStore, this.myState.like) await apilib.unupvote(this.rootStore, this.myState.upvote)
runInAction(() => { runInAction(() => {
this.likeCount-- this.upvoteCount--
this.myState.like = undefined this.myState.upvote = undefined
}) })
} else { }
const res = await apilib.like(this.rootStore, this.uri, this.cid) if (this.myState.downvote) {
await apilib.undownvote(this.rootStore, this.myState.downvote)
runInAction(() => { runInAction(() => {
this.likeCount++ this.downvoteCount--
this.myState.like = res.uri this.myState.downvote = undefined
})
}
}
async toggleUpvote() {
const wasntUpvoted = !this.myState.upvote
await this._clearVotes()
if (wasntUpvoted) {
const res = await apilib.upvote(this.rootStore, this.uri, this.cid)
runInAction(() => {
this.upvoteCount++
this.myState.upvote = res.uri
})
}
}
async toggleDownvote() {
const wasntDownvoted = !this.myState.downvote
await this._clearVotes()
if (wasntDownvoted) {
const res = await apilib.downvote(this.rootStore, this.uri, this.cid)
runInAction(() => {
this.downvoteCount++
this.myState.downvote = res.uri
}) })
} }
} }

View File

@ -26,7 +26,7 @@ export class MeModel {
this.did = sess.data.did || '' this.did = sess.data.did || ''
this.handle = sess.data.handle this.handle = sess.data.handle
const profile = await this.rootStore.api.app.bsky.actor.getProfile({ const profile = await this.rootStore.api.app.bsky.actor.getProfile({
user: this.did, actor: this.did,
}) })
runInAction(() => { runInAction(() => {
if (profile?.data) { if (profile?.data) {

View File

@ -57,8 +57,8 @@ export class NotificationsViewItemModel implements GroupedNotification {
} }
} }
get isLike() { get isUpvote() {
return this.reason === 'like' return this.reason === 'vote'
} }
get isRepost() { get isRepost() {

View File

@ -13,8 +13,9 @@ function* reactKeyGenerator(): Generator<string> {
} }
export class PostThreadViewPostMyStateModel { export class PostThreadViewPostMyStateModel {
like?: string
repost?: string repost?: string
upvote?: string
downvote?: string
constructor() { constructor() {
makeAutoObservable(this) makeAutoObservable(this)
@ -40,7 +41,8 @@ export class PostThreadViewPostModel implements GetPostThread.Post {
replyCount: number = 0 replyCount: number = 0
replies?: PostThreadViewPostModel[] replies?: PostThreadViewPostModel[]
repostCount: number = 0 repostCount: number = 0
likeCount: number = 0 upvoteCount: number = 0
downvoteCount: number = 0
indexedAt: string = '' indexedAt: string = ''
myState = new PostThreadViewPostMyStateModel() myState = new PostThreadViewPostMyStateModel()
@ -105,18 +107,43 @@ export class PostThreadViewPostModel implements GetPostThread.Post {
} }
} }
async toggleLike() { async _clearVotes() {
if (this.myState.like) { if (this.myState.upvote) {
await apilib.unlike(this.rootStore, this.myState.like) await apilib.unupvote(this.rootStore, this.myState.upvote)
runInAction(() => { runInAction(() => {
this.likeCount-- this.upvoteCount--
this.myState.like = undefined this.myState.upvote = undefined
}) })
} else { }
const res = await apilib.like(this.rootStore, this.uri, this.cid) if (this.myState.downvote) {
await apilib.undownvote(this.rootStore, this.myState.downvote)
runInAction(() => { runInAction(() => {
this.likeCount++ this.downvoteCount--
this.myState.like = res.uri this.myState.downvote = undefined
})
}
}
async toggleUpvote() {
const wasntUpvoted = !this.myState.upvote
await this._clearVotes()
if (wasntUpvoted) {
const res = await apilib.upvote(this.rootStore, this.uri, this.cid)
runInAction(() => {
this.upvoteCount++
this.myState.upvote = res.uri
})
}
}
async toggleDownvote() {
const wasntDownvoted = !this.myState.downvote
await this._clearVotes()
if (wasntDownvoted) {
const res = await apilib.downvote(this.rootStore, this.uri, this.cid)
runInAction(() => {
this.downvoteCount++
this.myState.downvote = res.uri
}) })
} }
} }

View File

@ -37,7 +37,7 @@ export class ProfileUiModel {
}, },
{autoBind: true}, {autoBind: true},
) )
this.profile = new ProfileViewModel(rootStore, {user: params.user}) this.profile = new ProfileViewModel(rootStore, {actor: params.user})
this.feed = new FeedModel(rootStore, 'author', { this.feed = new FeedModel(rootStore, 'author', {
author: params.user, author: params.user,
limit: 10, limit: 10,

View File

@ -1,45 +1,41 @@
import {makeAutoObservable, runInAction} from 'mobx' import {makeAutoObservable, runInAction} from 'mobx'
import {AtUri} from '../../third-party/uri' import {AtUri} from '../../third-party/uri'
import * as GetLikedBy from '../../third-party/api/src/client/types/app/bsky/feed/getLikedBy' import * as GetVotes from '../../third-party/api/src/client/types/app/bsky/feed/getVotes'
import {RootStoreModel} from './root-store' import {RootStoreModel} from './root-store'
type LikedByItem = GetLikedBy.OutputSchema['likedBy'][number] type VoteItem = GetVotes.OutputSchema['votes'][number]
export class LikedByViewItemModel implements LikedByItem { export class VotesViewItemModel implements VoteItem {
// ui state // ui state
_reactKey: string = '' _reactKey: string = ''
// data // data
did: string = '' direction: 'up' | 'down' = 'up'
handle: string = ''
displayName: string = ''
createdAt?: string
indexedAt: string = '' indexedAt: string = ''
createdAt: string = ''
actor: GetVotes.Actor = {did: '', handle: ''}
constructor(reactKey: string, v: LikedByItem) { constructor(reactKey: string, v: VoteItem) {
makeAutoObservable(this) makeAutoObservable(this)
this._reactKey = reactKey this._reactKey = reactKey
Object.assign(this, v) Object.assign(this, v)
} }
} }
export class LikedByViewModel { export class VotesViewModel {
// state // state
isLoading = false isLoading = false
isRefreshing = false isRefreshing = false
hasLoaded = false hasLoaded = false
error = '' error = ''
resolvedUri = '' resolvedUri = ''
params: GetLikedBy.QueryParams params: GetVotes.QueryParams
// data // data
uri: string = '' uri: string = ''
likedBy: LikedByViewItemModel[] = [] votes: VotesViewItemModel[] = []
constructor( constructor(public rootStore: RootStoreModel, params: GetVotes.QueryParams) {
public rootStore: RootStoreModel,
params: GetLikedBy.QueryParams,
) {
makeAutoObservable( makeAutoObservable(
this, this,
{ {
@ -113,7 +109,7 @@ export class LikedByViewModel {
private async _fetch(isRefreshing = false) { private async _fetch(isRefreshing = false) {
this._xLoading(isRefreshing) this._xLoading(isRefreshing)
try { try {
const res = await this.rootStore.api.app.bsky.feed.getLikedBy( const res = await this.rootStore.api.app.bsky.feed.getVotes(
Object.assign({}, this.params, {uri: this.resolvedUri}), Object.assign({}, this.params, {uri: this.resolvedUri}),
) )
this._replaceAll(res) this._replaceAll(res)
@ -123,15 +119,15 @@ export class LikedByViewModel {
} }
} }
private _replaceAll(res: GetLikedBy.Response) { private _replaceAll(res: GetVotes.Response) {
this.likedBy.length = 0 this.votes.length = 0
let counter = 0 let counter = 0
for (const item of res.data.likedBy) { for (const item of res.data.votes) {
this._append(counter++, item) this._append(counter++, item)
} }
} }
private _append(keyId: number, item: LikedByItem) { private _append(keyId: number, item: VoteItem) {
this.likedBy.push(new LikedByViewItemModel(`item-${keyId}`, item)) this.votes.push(new VotesViewItemModel(`item-${keyId}`, item))
} }
} }

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -21,25 +21,27 @@ import * as ComAtprotoSessionRefresh from './types/com/atproto/session/refresh';
import * as ComAtprotoSyncGetRepo from './types/com/atproto/sync/getRepo'; import * as ComAtprotoSyncGetRepo from './types/com/atproto/sync/getRepo';
import * as ComAtprotoSyncGetRoot from './types/com/atproto/sync/getRoot'; import * as ComAtprotoSyncGetRoot from './types/com/atproto/sync/getRoot';
import * as ComAtprotoSyncUpdateRepo from './types/com/atproto/sync/updateRepo'; import * as ComAtprotoSyncUpdateRepo from './types/com/atproto/sync/updateRepo';
import * as AppBskyActorCreateScene from './types/app/bsky/actor/createScene';
import * as AppBskyActorGetProfile from './types/app/bsky/actor/getProfile'; import * as AppBskyActorGetProfile from './types/app/bsky/actor/getProfile';
import * as AppBskyActorGetSuggestions from './types/app/bsky/actor/getSuggestions';
import * as AppBskyActorProfile from './types/app/bsky/actor/profile';
import * as AppBskyActorSearch from './types/app/bsky/actor/search'; import * as AppBskyActorSearch from './types/app/bsky/actor/search';
import * as AppBskyActorSearchTypeahead from './types/app/bsky/actor/searchTypeahead'; import * as AppBskyActorSearchTypeahead from './types/app/bsky/actor/searchTypeahead';
import * as AppBskyActorProfile from './types/app/bsky/actor/profile';
import * as AppBskyActorUpdateProfile from './types/app/bsky/actor/updateProfile'; import * as AppBskyActorUpdateProfile from './types/app/bsky/actor/updateProfile';
import * as AppBskyFeedGetAuthorFeed from './types/app/bsky/feed/getAuthorFeed'; import * as AppBskyFeedGetAuthorFeed from './types/app/bsky/feed/getAuthorFeed';
import * as AppBskyFeedGetLikedBy from './types/app/bsky/feed/getLikedBy';
import * as AppBskyFeedGetPostThread from './types/app/bsky/feed/getPostThread'; import * as AppBskyFeedGetPostThread from './types/app/bsky/feed/getPostThread';
import * as AppBskyFeedGetRepostedBy from './types/app/bsky/feed/getRepostedBy'; import * as AppBskyFeedGetRepostedBy from './types/app/bsky/feed/getRepostedBy';
import * as AppBskyFeedGetTimeline from './types/app/bsky/feed/getTimeline'; import * as AppBskyFeedGetTimeline from './types/app/bsky/feed/getTimeline';
import * as AppBskyFeedLike from './types/app/bsky/feed/like'; import * as AppBskyFeedGetVotes from './types/app/bsky/feed/getVotes';
import * as AppBskyFeedMediaEmbed from './types/app/bsky/feed/mediaEmbed'; import * as AppBskyFeedMediaEmbed from './types/app/bsky/feed/mediaEmbed';
import * as AppBskyFeedPost from './types/app/bsky/feed/post'; import * as AppBskyFeedPost from './types/app/bsky/feed/post';
import * as AppBskyFeedRepost from './types/app/bsky/feed/repost'; import * as AppBskyFeedRepost from './types/app/bsky/feed/repost';
import * as AppBskyFeedVote from './types/app/bsky/feed/vote';
import * as AppBskyGraphAssertion from './types/app/bsky/graph/assertion';
import * as AppBskyGraphConfirmation from './types/app/bsky/graph/confirmation';
import * as AppBskyGraphFollow from './types/app/bsky/graph/follow'; import * as AppBskyGraphFollow from './types/app/bsky/graph/follow';
import * as AppBskyGraphGetFollowers from './types/app/bsky/graph/getFollowers'; import * as AppBskyGraphGetFollowers from './types/app/bsky/graph/getFollowers';
import * as AppBskyGraphGetFollows from './types/app/bsky/graph/getFollows'; import * as AppBskyGraphGetFollows from './types/app/bsky/graph/getFollows';
import * as AppBskyGraphInvite from './types/app/bsky/graph/invite';
import * as AppBskyGraphInviteAccept from './types/app/bsky/graph/inviteAccept';
import * as AppBskyNotificationGetCount from './types/app/bsky/notification/getCount'; import * as AppBskyNotificationGetCount from './types/app/bsky/notification/getCount';
import * as AppBskyNotificationList from './types/app/bsky/notification/list'; import * as AppBskyNotificationList from './types/app/bsky/notification/list';
import * as AppBskyNotificationUpdateSeen from './types/app/bsky/notification/updateSeen'; import * as AppBskyNotificationUpdateSeen from './types/app/bsky/notification/updateSeen';
@ -66,29 +68,35 @@ export * as ComAtprotoSessionRefresh from './types/com/atproto/session/refresh';
export * as ComAtprotoSyncGetRepo from './types/com/atproto/sync/getRepo'; export * as ComAtprotoSyncGetRepo from './types/com/atproto/sync/getRepo';
export * as ComAtprotoSyncGetRoot from './types/com/atproto/sync/getRoot'; export * as ComAtprotoSyncGetRoot from './types/com/atproto/sync/getRoot';
export * as ComAtprotoSyncUpdateRepo from './types/com/atproto/sync/updateRepo'; export * as ComAtprotoSyncUpdateRepo from './types/com/atproto/sync/updateRepo';
export * as AppBskyActorCreateScene from './types/app/bsky/actor/createScene';
export * as AppBskyActorGetProfile from './types/app/bsky/actor/getProfile'; export * as AppBskyActorGetProfile from './types/app/bsky/actor/getProfile';
export * as AppBskyActorGetSuggestions from './types/app/bsky/actor/getSuggestions';
export * as AppBskyActorProfile from './types/app/bsky/actor/profile';
export * as AppBskyActorSearch from './types/app/bsky/actor/search'; export * as AppBskyActorSearch from './types/app/bsky/actor/search';
export * as AppBskyActorSearchTypeahead from './types/app/bsky/actor/searchTypeahead'; export * as AppBskyActorSearchTypeahead from './types/app/bsky/actor/searchTypeahead';
export * as AppBskyActorProfile from './types/app/bsky/actor/profile';
export * as AppBskyActorUpdateProfile from './types/app/bsky/actor/updateProfile'; export * as AppBskyActorUpdateProfile from './types/app/bsky/actor/updateProfile';
export * as AppBskyFeedGetAuthorFeed from './types/app/bsky/feed/getAuthorFeed'; export * as AppBskyFeedGetAuthorFeed from './types/app/bsky/feed/getAuthorFeed';
export * as AppBskyFeedGetLikedBy from './types/app/bsky/feed/getLikedBy';
export * as AppBskyFeedGetPostThread from './types/app/bsky/feed/getPostThread'; export * as AppBskyFeedGetPostThread from './types/app/bsky/feed/getPostThread';
export * as AppBskyFeedGetRepostedBy from './types/app/bsky/feed/getRepostedBy'; export * as AppBskyFeedGetRepostedBy from './types/app/bsky/feed/getRepostedBy';
export * as AppBskyFeedGetTimeline from './types/app/bsky/feed/getTimeline'; export * as AppBskyFeedGetTimeline from './types/app/bsky/feed/getTimeline';
export * as AppBskyFeedLike from './types/app/bsky/feed/like'; export * as AppBskyFeedGetVotes from './types/app/bsky/feed/getVotes';
export * as AppBskyFeedMediaEmbed from './types/app/bsky/feed/mediaEmbed'; export * as AppBskyFeedMediaEmbed from './types/app/bsky/feed/mediaEmbed';
export * as AppBskyFeedPost from './types/app/bsky/feed/post'; export * as AppBskyFeedPost from './types/app/bsky/feed/post';
export * as AppBskyFeedRepost from './types/app/bsky/feed/repost'; export * as AppBskyFeedRepost from './types/app/bsky/feed/repost';
export * as AppBskyFeedVote from './types/app/bsky/feed/vote';
export * as AppBskyGraphAssertion from './types/app/bsky/graph/assertion';
export * as AppBskyGraphConfirmation from './types/app/bsky/graph/confirmation';
export * as AppBskyGraphFollow from './types/app/bsky/graph/follow'; export * as AppBskyGraphFollow from './types/app/bsky/graph/follow';
export * as AppBskyGraphGetFollowers from './types/app/bsky/graph/getFollowers'; export * as AppBskyGraphGetFollowers from './types/app/bsky/graph/getFollowers';
export * as AppBskyGraphGetFollows from './types/app/bsky/graph/getFollows'; export * as AppBskyGraphGetFollows from './types/app/bsky/graph/getFollows';
export * as AppBskyGraphInvite from './types/app/bsky/graph/invite';
export * as AppBskyGraphInviteAccept from './types/app/bsky/graph/inviteAccept';
export * as AppBskyNotificationGetCount from './types/app/bsky/notification/getCount'; export * as AppBskyNotificationGetCount from './types/app/bsky/notification/getCount';
export * as AppBskyNotificationList from './types/app/bsky/notification/list'; export * as AppBskyNotificationList from './types/app/bsky/notification/list';
export * as AppBskyNotificationUpdateSeen from './types/app/bsky/notification/updateSeen'; export * as AppBskyNotificationUpdateSeen from './types/app/bsky/notification/updateSeen';
export * as AppBskySystemDeclaration from './types/app/bsky/system/declaration'; export * as AppBskySystemDeclaration from './types/app/bsky/system/declaration';
export declare const APP_BSKY_GRAPH: {
AssertCreator: string;
AssertMember: string;
};
export declare const APP_BSKY_SYSTEM: { export declare const APP_BSKY_SYSTEM: {
ActorScene: string; ActorScene: string;
ActorUser: string; ActorUser: string;
@ -187,7 +195,9 @@ export declare class ActorNS {
_service: ServiceClient; _service: ServiceClient;
profile: ProfileRecord; profile: ProfileRecord;
constructor(service: ServiceClient); constructor(service: ServiceClient);
createScene(data?: AppBskyActorCreateScene.InputSchema, opts?: AppBskyActorCreateScene.CallOptions): Promise<AppBskyActorCreateScene.Response>;
getProfile(params?: AppBskyActorGetProfile.QueryParams, opts?: AppBskyActorGetProfile.CallOptions): Promise<AppBskyActorGetProfile.Response>; getProfile(params?: AppBskyActorGetProfile.QueryParams, opts?: AppBskyActorGetProfile.CallOptions): Promise<AppBskyActorGetProfile.Response>;
getSuggestions(params?: AppBskyActorGetSuggestions.QueryParams, opts?: AppBskyActorGetSuggestions.CallOptions): Promise<AppBskyActorGetSuggestions.Response>;
search(params?: AppBskyActorSearch.QueryParams, opts?: AppBskyActorSearch.CallOptions): Promise<AppBskyActorSearch.Response>; search(params?: AppBskyActorSearch.QueryParams, opts?: AppBskyActorSearch.CallOptions): Promise<AppBskyActorSearch.Response>;
searchTypeahead(params?: AppBskyActorSearchTypeahead.QueryParams, opts?: AppBskyActorSearchTypeahead.CallOptions): Promise<AppBskyActorSearchTypeahead.Response>; searchTypeahead(params?: AppBskyActorSearchTypeahead.QueryParams, opts?: AppBskyActorSearchTypeahead.CallOptions): Promise<AppBskyActorSearchTypeahead.Response>;
updateProfile(data?: AppBskyActorUpdateProfile.InputSchema, opts?: AppBskyActorUpdateProfile.CallOptions): Promise<AppBskyActorUpdateProfile.Response>; updateProfile(data?: AppBskyActorUpdateProfile.InputSchema, opts?: AppBskyActorUpdateProfile.CallOptions): Promise<AppBskyActorUpdateProfile.Response>;
@ -215,37 +225,16 @@ export declare class ProfileRecord {
} }
export declare class FeedNS { export declare class FeedNS {
_service: ServiceClient; _service: ServiceClient;
like: LikeRecord;
mediaEmbed: MediaEmbedRecord; mediaEmbed: MediaEmbedRecord;
post: PostRecord; post: PostRecord;
repost: RepostRecord; repost: RepostRecord;
vote: VoteRecord;
constructor(service: ServiceClient); constructor(service: ServiceClient);
getAuthorFeed(params?: AppBskyFeedGetAuthorFeed.QueryParams, opts?: AppBskyFeedGetAuthorFeed.CallOptions): Promise<AppBskyFeedGetAuthorFeed.Response>; getAuthorFeed(params?: AppBskyFeedGetAuthorFeed.QueryParams, opts?: AppBskyFeedGetAuthorFeed.CallOptions): Promise<AppBskyFeedGetAuthorFeed.Response>;
getLikedBy(params?: AppBskyFeedGetLikedBy.QueryParams, opts?: AppBskyFeedGetLikedBy.CallOptions): Promise<AppBskyFeedGetLikedBy.Response>;
getPostThread(params?: AppBskyFeedGetPostThread.QueryParams, opts?: AppBskyFeedGetPostThread.CallOptions): Promise<AppBskyFeedGetPostThread.Response>; getPostThread(params?: AppBskyFeedGetPostThread.QueryParams, opts?: AppBskyFeedGetPostThread.CallOptions): Promise<AppBskyFeedGetPostThread.Response>;
getRepostedBy(params?: AppBskyFeedGetRepostedBy.QueryParams, opts?: AppBskyFeedGetRepostedBy.CallOptions): Promise<AppBskyFeedGetRepostedBy.Response>; getRepostedBy(params?: AppBskyFeedGetRepostedBy.QueryParams, opts?: AppBskyFeedGetRepostedBy.CallOptions): Promise<AppBskyFeedGetRepostedBy.Response>;
getTimeline(params?: AppBskyFeedGetTimeline.QueryParams, opts?: AppBskyFeedGetTimeline.CallOptions): Promise<AppBskyFeedGetTimeline.Response>; getTimeline(params?: AppBskyFeedGetTimeline.QueryParams, opts?: AppBskyFeedGetTimeline.CallOptions): Promise<AppBskyFeedGetTimeline.Response>;
} getVotes(params?: AppBskyFeedGetVotes.QueryParams, opts?: AppBskyFeedGetVotes.CallOptions): Promise<AppBskyFeedGetVotes.Response>;
export declare class LikeRecord {
_service: ServiceClient;
constructor(service: ServiceClient);
list(params: Omit<ComAtprotoRepoListRecords.QueryParams, 'collection'>): Promise<{
cursor?: string;
records: {
uri: string;
value: AppBskyFeedLike.Record;
}[];
}>;
get(params: Omit<ComAtprotoRepoGetRecord.QueryParams, 'collection'>): Promise<{
uri: string;
cid: string;
value: AppBskyFeedLike.Record;
}>;
create(params: Omit<ComAtprotoRepoCreateRecord.InputSchema, 'collection' | 'record'>, record: AppBskyFeedLike.Record, headers?: Record<string, string>): Promise<{
uri: string;
cid: string;
}>;
delete(params: Omit<ComAtprotoRepoDeleteRecord.QueryParams, 'collection'>, headers?: Record<string, string>): Promise<void>;
} }
export declare class MediaEmbedRecord { export declare class MediaEmbedRecord {
_service: ServiceClient; _service: ServiceClient;
@ -310,15 +299,78 @@ export declare class RepostRecord {
}>; }>;
delete(params: Omit<ComAtprotoRepoDeleteRecord.QueryParams, 'collection'>, headers?: Record<string, string>): Promise<void>; delete(params: Omit<ComAtprotoRepoDeleteRecord.QueryParams, 'collection'>, headers?: Record<string, string>): Promise<void>;
} }
export declare class VoteRecord {
_service: ServiceClient;
constructor(service: ServiceClient);
list(params: Omit<ComAtprotoRepoListRecords.QueryParams, 'collection'>): Promise<{
cursor?: string;
records: {
uri: string;
value: AppBskyFeedVote.Record;
}[];
}>;
get(params: Omit<ComAtprotoRepoGetRecord.QueryParams, 'collection'>): Promise<{
uri: string;
cid: string;
value: AppBskyFeedVote.Record;
}>;
create(params: Omit<ComAtprotoRepoCreateRecord.InputSchema, 'collection' | 'record'>, record: AppBskyFeedVote.Record, headers?: Record<string, string>): Promise<{
uri: string;
cid: string;
}>;
delete(params: Omit<ComAtprotoRepoDeleteRecord.QueryParams, 'collection'>, headers?: Record<string, string>): Promise<void>;
}
export declare class GraphNS { export declare class GraphNS {
_service: ServiceClient; _service: ServiceClient;
assertion: AssertionRecord;
confirmation: ConfirmationRecord;
follow: FollowRecord; follow: FollowRecord;
invite: InviteRecord;
inviteAccept: InviteAcceptRecord;
constructor(service: ServiceClient); constructor(service: ServiceClient);
getFollowers(params?: AppBskyGraphGetFollowers.QueryParams, opts?: AppBskyGraphGetFollowers.CallOptions): Promise<AppBskyGraphGetFollowers.Response>; getFollowers(params?: AppBskyGraphGetFollowers.QueryParams, opts?: AppBskyGraphGetFollowers.CallOptions): Promise<AppBskyGraphGetFollowers.Response>;
getFollows(params?: AppBskyGraphGetFollows.QueryParams, opts?: AppBskyGraphGetFollows.CallOptions): Promise<AppBskyGraphGetFollows.Response>; getFollows(params?: AppBskyGraphGetFollows.QueryParams, opts?: AppBskyGraphGetFollows.CallOptions): Promise<AppBskyGraphGetFollows.Response>;
} }
export declare class AssertionRecord {
_service: ServiceClient;
constructor(service: ServiceClient);
list(params: Omit<ComAtprotoRepoListRecords.QueryParams, 'collection'>): Promise<{
cursor?: string;
records: {
uri: string;
value: AppBskyGraphAssertion.Record;
}[];
}>;
get(params: Omit<ComAtprotoRepoGetRecord.QueryParams, 'collection'>): Promise<{
uri: string;
cid: string;
value: AppBskyGraphAssertion.Record;
}>;
create(params: Omit<ComAtprotoRepoCreateRecord.InputSchema, 'collection' | 'record'>, record: AppBskyGraphAssertion.Record, headers?: Record<string, string>): Promise<{
uri: string;
cid: string;
}>;
delete(params: Omit<ComAtprotoRepoDeleteRecord.QueryParams, 'collection'>, headers?: Record<string, string>): Promise<void>;
}
export declare class ConfirmationRecord {
_service: ServiceClient;
constructor(service: ServiceClient);
list(params: Omit<ComAtprotoRepoListRecords.QueryParams, 'collection'>): Promise<{
cursor?: string;
records: {
uri: string;
value: AppBskyGraphConfirmation.Record;
}[];
}>;
get(params: Omit<ComAtprotoRepoGetRecord.QueryParams, 'collection'>): Promise<{
uri: string;
cid: string;
value: AppBskyGraphConfirmation.Record;
}>;
create(params: Omit<ComAtprotoRepoCreateRecord.InputSchema, 'collection' | 'record'>, record: AppBskyGraphConfirmation.Record, headers?: Record<string, string>): Promise<{
uri: string;
cid: string;
}>;
delete(params: Omit<ComAtprotoRepoDeleteRecord.QueryParams, 'collection'>, headers?: Record<string, string>): Promise<void>;
}
export declare class FollowRecord { export declare class FollowRecord {
_service: ServiceClient; _service: ServiceClient;
constructor(service: ServiceClient); constructor(service: ServiceClient);
@ -340,48 +392,6 @@ export declare class FollowRecord {
}>; }>;
delete(params: Omit<ComAtprotoRepoDeleteRecord.QueryParams, 'collection'>, headers?: Record<string, string>): Promise<void>; delete(params: Omit<ComAtprotoRepoDeleteRecord.QueryParams, 'collection'>, headers?: Record<string, string>): Promise<void>;
} }
export declare class InviteRecord {
_service: ServiceClient;
constructor(service: ServiceClient);
list(params: Omit<ComAtprotoRepoListRecords.QueryParams, 'collection'>): Promise<{
cursor?: string;
records: {
uri: string;
value: AppBskyGraphInvite.Record;
}[];
}>;
get(params: Omit<ComAtprotoRepoGetRecord.QueryParams, 'collection'>): Promise<{
uri: string;
cid: string;
value: AppBskyGraphInvite.Record;
}>;
create(params: Omit<ComAtprotoRepoCreateRecord.InputSchema, 'collection' | 'record'>, record: AppBskyGraphInvite.Record, headers?: Record<string, string>): Promise<{
uri: string;
cid: string;
}>;
delete(params: Omit<ComAtprotoRepoDeleteRecord.QueryParams, 'collection'>, headers?: Record<string, string>): Promise<void>;
}
export declare class InviteAcceptRecord {
_service: ServiceClient;
constructor(service: ServiceClient);
list(params: Omit<ComAtprotoRepoListRecords.QueryParams, 'collection'>): Promise<{
cursor?: string;
records: {
uri: string;
value: AppBskyGraphInviteAccept.Record;
}[];
}>;
get(params: Omit<ComAtprotoRepoGetRecord.QueryParams, 'collection'>): Promise<{
uri: string;
cid: string;
value: AppBskyGraphInviteAccept.Record;
}>;
create(params: Omit<ComAtprotoRepoCreateRecord.InputSchema, 'collection' | 'record'>, record: AppBskyGraphInviteAccept.Record, headers?: Record<string, string>): Promise<{
uri: string;
cid: string;
}>;
delete(params: Omit<ComAtprotoRepoDeleteRecord.QueryParams, 'collection'>, headers?: Record<string, string>): Promise<void>;
}
export declare class NotificationNS { export declare class NotificationNS {
_service: ServiceClient; _service: ServiceClient;
constructor(service: ServiceClient); constructor(service: ServiceClient);

View File

@ -3,13 +3,13 @@ export declare const methodSchemaDict: Record<string, MethodSchema>;
export declare const methodSchemas: MethodSchema[]; export declare const methodSchemas: MethodSchema[];
export declare const ids: { export declare const ids: {
AppBskyActorProfile: string; AppBskyActorProfile: string;
AppBskyFeedLike: string;
AppBskyFeedMediaEmbed: string; AppBskyFeedMediaEmbed: string;
AppBskyFeedPost: string; AppBskyFeedPost: string;
AppBskyFeedRepost: string; AppBskyFeedRepost: string;
AppBskyFeedVote: string;
AppBskyGraphAssertion: string;
AppBskyGraphConfirmation: string;
AppBskyGraphFollow: string; AppBskyGraphFollow: string;
AppBskyGraphInvite: string;
AppBskyGraphInviteAccept: string;
AppBskySystemDeclaration: string; AppBskySystemDeclaration: string;
}; };
export declare const recordSchemaDict: Record<string, RecordSchema>; export declare const recordSchemaDict: Record<string, RecordSchema>;

View File

@ -0,0 +1,29 @@
import { Headers, XRPCError } from '@atproto/xrpc';
export interface QueryParams {
}
export interface CallOptions {
headers?: Headers;
qp?: QueryParams;
encoding: 'application/json';
}
export interface InputSchema {
handle: string;
recoveryKey?: string;
}
export interface OutputSchema {
handle: string;
did: string;
declarationCid: string;
}
export interface Response {
success: boolean;
headers: Headers;
data: OutputSchema;
}
export declare class InvalidHandleError extends XRPCError {
constructor(src: XRPCError);
}
export declare class HandleNotAvailableError extends XRPCError {
constructor(src: XRPCError);
}
export declare function toKnownErr(e: any): any;

View File

@ -1,18 +1,23 @@
import { Headers } from '@atproto/xrpc'; import { Headers } from '@atproto/xrpc';
export interface QueryParams { export interface QueryParams {
user: string; actor: string;
} }
export interface CallOptions { export interface CallOptions {
headers?: Headers; headers?: Headers;
} }
export declare type InputSchema = undefined; export declare type InputSchema = undefined;
export declare type ActorKnown = 'app.bsky.system.actorUser' | 'app.bsky.system.actorScene';
export declare type ActorUnknown = string;
export interface OutputSchema { export interface OutputSchema {
did: string; did: string;
handle: string; handle: string;
actorType: ActorKnown | ActorUnknown;
creator: string;
displayName?: string; displayName?: string;
description?: string; description?: string;
followersCount: number; followersCount: number;
followsCount: number; followsCount: number;
membersCount: number;
postsCount: number; postsCount: number;
myState?: { myState?: {
follow?: string; follow?: string;

View File

@ -0,0 +1,29 @@
import { Headers } from '@atproto/xrpc';
export interface QueryParams {
limit?: number;
cursor?: string;
}
export interface CallOptions {
headers?: Headers;
}
export declare type InputSchema = undefined;
export interface OutputSchema {
cursor?: string;
actors: {
did: string;
handle: string;
actorType: string;
displayName?: string;
description?: string;
indexedAt?: string;
myState?: {
follow?: string;
};
}[];
}
export interface Response {
success: boolean;
headers: Headers;
data: OutputSchema;
}
export declare function toKnownErr(e: any): any;

View File

@ -21,11 +21,13 @@ export interface FeedItem {
embed?: RecordEmbed | ExternalEmbed | UnknownEmbed; embed?: RecordEmbed | ExternalEmbed | UnknownEmbed;
replyCount: number; replyCount: number;
repostCount: number; repostCount: number;
likeCount: number; upvoteCount: number;
downvoteCount: number;
indexedAt: string; indexedAt: string;
myState?: { myState?: {
repost?: string; repost?: string;
like?: string; upvote?: string;
downvote?: string;
}; };
} }
export interface User { export interface User {

View File

@ -19,12 +19,14 @@ export interface Post {
parent?: Post; parent?: Post;
replyCount: number; replyCount: number;
replies?: Post[]; replies?: Post[];
likeCount: number;
repostCount: number; repostCount: number;
upvoteCount: number;
downvoteCount: number;
indexedAt: string; indexedAt: string;
myState?: { myState?: {
repost?: string; repost?: string;
like?: string; upvote?: string;
downvote?: string;
}; };
} }
export interface User { export interface User {

View File

@ -21,11 +21,13 @@ export interface FeedItem {
embed?: RecordEmbed | ExternalEmbed | UnknownEmbed; embed?: RecordEmbed | ExternalEmbed | UnknownEmbed;
replyCount: number; replyCount: number;
repostCount: number; repostCount: number;
likeCount: number; upvoteCount: number;
downvoteCount: number;
indexedAt: string; indexedAt: string;
myState?: { myState?: {
repost?: string; repost?: string;
like?: string; upvote?: string;
downvote?: string;
}; };
} }
export interface User { export interface User {

View File

@ -0,0 +1,34 @@
import { Headers } from '@atproto/xrpc';
export interface QueryParams {
uri: string;
cid?: string;
direction?: string;
limit?: number;
before?: string;
}
export interface CallOptions {
headers?: Headers;
}
export declare type InputSchema = undefined;
export interface OutputSchema {
uri: string;
cid?: string;
cursor?: string;
votes: {
direction: 'up' | 'down';
indexedAt: string;
createdAt: string;
actor: Actor;
}[];
}
export interface Actor {
did: string;
handle: string;
displayName?: string;
}
export interface Response {
success: boolean;
headers: Headers;
data: OutputSchema;
}
export declare function toKnownErr(e: any): any;

View File

@ -0,0 +1,11 @@
export interface Record {
subject: Subject;
direction: 'up' | 'down';
createdAt: string;
[k: string]: unknown;
}
export interface Subject {
uri: string;
cid: string;
[k: string]: unknown;
}

View File

@ -0,0 +1,10 @@
export interface Record {
assertion: string;
subject: {
did: string;
declarationCid: string;
[k: string]: unknown;
};
createdAt: string;
[k: string]: unknown;
}

View File

@ -0,0 +1,14 @@
export interface Record {
originator: {
did: string;
declarationCid: string;
[k: string]: unknown;
};
assertion: {
uri: string;
cid: string;
[k: string]: unknown;
};
createdAt: string;
[k: string]: unknown;
}

File diff suppressed because one or more lines are too long

View File

@ -6,6 +6,7 @@ import {FontAwesomeIcon, Props} from '@fortawesome/react-native-fontawesome'
import {NotificationsViewItemModel} from '../../../state/models/notifications-view' import {NotificationsViewItemModel} from '../../../state/models/notifications-view'
import {s, colors} from '../../lib/styles' import {s, colors} from '../../lib/styles'
import {ago, pluralize} from '../../lib/strings' import {ago, pluralize} from '../../lib/strings'
import {UpIconSolid} from '../../lib/icons'
import {UserAvatar} from '../util/UserAvatar' import {UserAvatar} from '../util/UserAvatar'
import {PostText} from '../post/PostText' import {PostText} from '../post/PostText'
import {Post} from '../post/Post' import {Post} from '../post/Post'
@ -19,7 +20,7 @@ export const FeedItem = observer(function FeedItem({
item: NotificationsViewItemModel item: NotificationsViewItemModel
}) { }) {
const itemHref = useMemo(() => { const itemHref = useMemo(() => {
if (item.isLike || item.isRepost) { if (item.isUpvote || item.isRepost) {
const urip = new AtUri(item.subjectUri) const urip = new AtUri(item.subjectUri)
return `/profile/${urip.host}/post/${urip.rkey}` return `/profile/${urip.host}/post/${urip.rkey}`
} else if (item.isFollow) { } else if (item.isFollow) {
@ -31,7 +32,7 @@ export const FeedItem = observer(function FeedItem({
return '' return ''
}, [item]) }, [item])
const itemTitle = useMemo(() => { const itemTitle = useMemo(() => {
if (item.isLike || item.isRepost) { if (item.isUpvote || item.isRepost) {
return 'Post' return 'Post'
} else if (item.isFollow) { } else if (item.isFollow) {
return item.author.handle return item.author.handle
@ -55,16 +56,16 @@ export const FeedItem = observer(function FeedItem({
} }
let action = '' let action = ''
let icon: Props['icon'] let icon: Props['icon'] | 'UpIconSolid'
let iconStyle: Props['style'] = [] let iconStyle: Props['style'] = []
if (item.isLike) { if (item.isUpvote) {
action = 'liked your post' action = 'upvoted your post'
icon = ['fas', 'heart'] icon = 'UpIconSolid'
iconStyle = [s.blue3] iconStyle = [s.red3, {position: 'relative', top: -4}]
} else if (item.isRepost) { } else if (item.isRepost) {
action = 'reposted your post' action = 'reposted your post'
icon = 'retweet' icon = 'retweet'
iconStyle = [s.blue3] iconStyle = [s.green3]
} else if (item.isReply) { } else if (item.isReply) {
action = 'replied to your post' action = 'replied to your post'
icon = ['far', 'comment'] icon = ['far', 'comment']
@ -100,11 +101,15 @@ export const FeedItem = observer(function FeedItem({
title={itemTitle}> title={itemTitle}>
<View style={styles.layout}> <View style={styles.layout}>
<View style={styles.layoutIcon}> <View style={styles.layoutIcon}>
<FontAwesomeIcon {icon === 'UpIconSolid' ? (
icon={icon} <UpIconSolid size={26} style={[styles.icon, ...iconStyle]} />
size={22} ) : (
style={[styles.icon, ...iconStyle]} <FontAwesomeIcon
/> icon={icon}
size={22}
style={[styles.icon, ...iconStyle]}
/>
)}
</View> </View>
<View style={styles.layoutContent}> <View style={styles.layoutContent}>
<View style={styles.avis}> <View style={styles.avis}>
@ -150,7 +155,7 @@ export const FeedItem = observer(function FeedItem({
{ago(item.indexedAt)} {ago(item.indexedAt)}
</Text> </Text>
</View> </View>
{item.isLike || item.isRepost ? ( {item.isUpvote || item.isRepost ? (
<PostText uri={item.subjectUri} style={[s.gray5]} /> <PostText uri={item.subjectUri} style={[s.gray5]} />
) : ( ) : (
<></> <></>

View File

@ -28,7 +28,8 @@ export const PostThreadItem = observer(function PostThreadItem({
}) { }) {
const store = useStores() const store = useStores()
const record = item.record as unknown as PostType.Record const record = item.record as unknown as PostType.Record
const hasEngagement = item.likeCount || item.repostCount const hasEngagement =
item.upvoteCount || item.downvoteCount || item.repostCount
const itemHref = useMemo(() => { const itemHref = useMemo(() => {
const urip = new AtUri(item.uri) const urip = new AtUri(item.uri)
@ -37,11 +38,16 @@ export const PostThreadItem = observer(function PostThreadItem({
const itemTitle = `Post by ${item.author.handle}` const itemTitle = `Post by ${item.author.handle}`
const authorHref = `/profile/${item.author.handle}` const authorHref = `/profile/${item.author.handle}`
const authorTitle = item.author.handle const authorTitle = item.author.handle
const likesHref = useMemo(() => { const upvotesHref = useMemo(() => {
const urip = new AtUri(item.uri) const urip = new AtUri(item.uri)
return `/profile/${item.author.handle}/post/${urip.rkey}/liked-by` return `/profile/${item.author.handle}/post/${urip.rkey}/upvoted-by`
}, [item.uri, item.author.handle]) }, [item.uri, item.author.handle])
const likesTitle = 'Likes on this post' const upvotesTitle = 'Upvotes on this post'
const downvotesHref = useMemo(() => {
const urip = new AtUri(item.uri)
return `/profile/${item.author.handle}/post/${urip.rkey}/downvoted-by`
}, [item.uri, item.author.handle])
const downvotesTitle = 'Downvotes on this post'
const repostsHref = useMemo(() => { const repostsHref = useMemo(() => {
const urip = new AtUri(item.uri) const urip = new AtUri(item.uri)
return `/profile/${item.author.handle}/post/${urip.rkey}/reposted-by` return `/profile/${item.author.handle}/post/${urip.rkey}/reposted-by`
@ -59,10 +65,15 @@ export const PostThreadItem = observer(function PostThreadItem({
.toggleRepost() .toggleRepost()
.catch(e => console.error('Failed to toggle repost', record, e)) .catch(e => console.error('Failed to toggle repost', record, e))
} }
const onPressToggleLike = () => { const onPressToggleUpvote = () => {
item item
.toggleLike() .toggleUpvote()
.catch(e => console.error('Failed to toggle like', record, e)) .catch(e => console.error('Failed to toggle upvote', record, e))
}
const onPressToggleDownvote = () => {
item
.toggleDownvote()
.catch(e => console.error('Failed to toggle downvote', record, e))
} }
if (item._isHighlightedPost) { if (item._isHighlightedPost) {
@ -135,16 +146,31 @@ export const PostThreadItem = observer(function PostThreadItem({
) : ( ) : (
<></> <></>
)} )}
{item.likeCount ? ( {item.upvoteCount ? (
<Link <Link
style={styles.expandedInfoItem} style={styles.expandedInfoItem}
href={likesHref} href={upvotesHref}
title={likesTitle}> title={upvotesTitle}>
<Text style={[s.gray5, s.semiBold, s.f16]}> <Text style={[s.gray5, s.semiBold, s.f16]}>
<Text style={[s.bold, s.black, s.f16]}> <Text style={[s.bold, s.black, s.f16]}>
{item.likeCount} {item.upvoteCount}
</Text>{' '} </Text>{' '}
{pluralize(item.likeCount, 'like')} {pluralize(item.upvoteCount, 'upvote')}
</Text>
</Link>
) : (
<></>
)}
{item.downvoteCount ? (
<Link
style={styles.expandedInfoItem}
href={downvotesHref}
title={downvotesTitle}>
<Text style={[s.gray5, s.semiBold, s.f16]}>
<Text style={[s.bold, s.black, s.f16]}>
{item.downvoteCount}
</Text>{' '}
{pluralize(item.downvoteCount, 'downvote')}
</Text> </Text>
</Link> </Link>
) : ( ) : (
@ -158,12 +184,15 @@ export const PostThreadItem = observer(function PostThreadItem({
<PostCtrls <PostCtrls
replyCount={item.replyCount} replyCount={item.replyCount}
repostCount={item.repostCount} repostCount={item.repostCount}
likeCount={item.likeCount} upvoteCount={item.upvoteCount}
downvoteCount={item.downvoteCount}
isReposted={!!item.myState.repost} isReposted={!!item.myState.repost}
isLiked={!!item.myState.like} isUpvoted={!!item.myState.upvote}
isDownvoted={!!item.myState.downvote}
onPressReply={onPressReply} onPressReply={onPressReply}
onPressToggleRepost={onPressToggleRepost} onPressToggleRepost={onPressToggleRepost}
onPressToggleLike={onPressToggleLike} onPressToggleUpvote={onPressToggleUpvote}
onPressToggleDownvote={onPressToggleDownvote}
/> />
</View> </View>
</View> </View>
@ -260,12 +289,15 @@ export const PostThreadItem = observer(function PostThreadItem({
<PostCtrls <PostCtrls
replyCount={item.replyCount} replyCount={item.replyCount}
repostCount={item.repostCount} repostCount={item.repostCount}
likeCount={item.likeCount} upvoteCount={item.upvoteCount}
downvoteCount={item.downvoteCount}
isReposted={!!item.myState.repost} isReposted={!!item.myState.repost}
isLiked={!!item.myState.like} isUpvoted={!!item.myState.upvote}
isDownvoted={!!item.myState.downvote}
onPressReply={onPressReply} onPressReply={onPressReply}
onPressToggleRepost={onPressToggleRepost} onPressToggleRepost={onPressToggleRepost}
onPressToggleLike={onPressToggleLike} onPressToggleUpvote={onPressToggleUpvote}
onPressToggleDownvote={onPressToggleDownvote}
/> />
</View> </View>
</View> </View>

View File

@ -2,27 +2,33 @@ import React, {useState, useEffect} from 'react'
import {observer} from 'mobx-react-lite' import {observer} from 'mobx-react-lite'
import {ActivityIndicator, FlatList, StyleSheet, Text, View} from 'react-native' import {ActivityIndicator, FlatList, StyleSheet, Text, View} from 'react-native'
import { import {
LikedByViewModel, VotesViewModel,
LikedByViewItemModel, VotesViewItemModel,
} from '../../../state/models/liked-by-view' } from '../../../state/models/votes-view'
import {Link} from '../util/Link' import {Link} from '../util/Link'
import {UserAvatar} from '../util/UserAvatar' import {UserAvatar} from '../util/UserAvatar'
import {useStores} from '../../../state' import {useStores} from '../../../state'
import {s, colors} from '../../lib/styles' import {s, colors} from '../../lib/styles'
export const PostLikedBy = observer(function PostLikedBy({uri}: {uri: string}) { export const PostVotedBy = observer(function PostVotedBy({
uri,
direction,
}: {
uri: string
direction: 'up' | 'down'
}) {
const store = useStores() const store = useStores()
const [view, setView] = useState<LikedByViewModel | undefined>() const [view, setView] = useState<VotesViewModel | undefined>()
useEffect(() => { useEffect(() => {
if (view?.params.uri === uri) { if (view?.params.uri === uri) {
console.log('Liked by doing nothing') console.log('Voted by doing nothing')
return // no change needed? or trigger refresh? return // no change needed? or trigger refresh?
} }
console.log('Fetching Liked by', uri) console.log('Fetching voted by', uri)
const newView = new LikedByViewModel(store, {uri}) const newView = new VotesViewModel(store, {uri, direction})
setView(newView) setView(newView)
newView.setup().catch(err => console.error('Failed to fetch liked by', err)) newView.setup().catch(err => console.error('Failed to fetch voted by', err))
}, [uri, view?.params.uri, store]) }, [uri, view?.params.uri, store])
// loading // loading
@ -51,13 +57,13 @@ export const PostLikedBy = observer(function PostLikedBy({uri}: {uri: string}) {
// loaded // loaded
// = // =
const renderItem = ({item}: {item: LikedByViewItemModel}) => ( const renderItem = ({item}: {item: VotesViewItemModel}) => (
<LikedByItem item={item} /> <LikedByItem item={item} />
) )
return ( return (
<View> <View>
<FlatList <FlatList
data={view.likedBy} data={view.votes}
keyExtractor={item => item._reactKey} keyExtractor={item => item._reactKey}
renderItem={renderItem} renderItem={renderItem}
/> />
@ -65,23 +71,23 @@ export const PostLikedBy = observer(function PostLikedBy({uri}: {uri: string}) {
) )
}) })
const LikedByItem = ({item}: {item: LikedByViewItemModel}) => { const LikedByItem = ({item}: {item: VotesViewItemModel}) => {
return ( return (
<Link <Link
style={styles.outer} style={styles.outer}
href={`/profile/${item.handle}`} href={`/profile/${item.actor.handle}`}
title={item.handle}> title={item.actor.handle}>
<View style={styles.layout}> <View style={styles.layout}>
<View style={styles.layoutAvi}> <View style={styles.layoutAvi}>
<UserAvatar <UserAvatar
size={40} size={40}
displayName={item.displayName} displayName={item.actor.displayName}
handle={item.handle} handle={item.actor.handle}
/> />
</View> </View>
<View style={styles.layoutContent}> <View style={styles.layoutContent}>
<Text style={[s.f15, s.bold]}>{item.displayName}</Text> <Text style={[s.f15, s.bold]}>{item.actor.displayName}</Text>
<Text style={[s.f14, s.gray5]}>@{item.handle}</Text> <Text style={[s.f14, s.gray5]}>@{item.actor.handle}</Text>
</View> </View>
</View> </View>
</Link> </Link>

View File

@ -78,10 +78,15 @@ export const Post = observer(function Post({uri}: {uri: string}) {
.toggleRepost() .toggleRepost()
.catch(e => console.error('Failed to toggle repost', record, e)) .catch(e => console.error('Failed to toggle repost', record, e))
} }
const onPressToggleLike = () => { const onPressToggleUpvote = () => {
item item
.toggleLike() .toggleUpvote()
.catch(e => console.error('Failed to toggle like', record, e)) .catch(e => console.error('Failed to toggle upvote', record, e))
}
const onPressToggleDownvote = () => {
item
.toggleDownvote()
.catch(e => console.error('Failed to toggle downvote', record, e))
} }
return ( return (
@ -129,12 +134,15 @@ export const Post = observer(function Post({uri}: {uri: string}) {
<PostCtrls <PostCtrls
replyCount={item.replyCount} replyCount={item.replyCount}
repostCount={item.repostCount} repostCount={item.repostCount}
likeCount={item.likeCount} upvoteCount={item.upvoteCount}
downvoteCount={item.downvoteCount}
isReposted={!!item.myState.repost} isReposted={!!item.myState.repost}
isLiked={!!item.myState.like} isUpvoted={!!item.myState.upvote}
isDownvoted={!!item.myState.downvote}
onPressReply={onPressReply} onPressReply={onPressReply}
onPressToggleRepost={onPressToggleRepost} onPressToggleRepost={onPressToggleRepost}
onPressToggleLike={onPressToggleLike} onPressToggleUpvote={onPressToggleUpvote}
onPressToggleDownvote={onPressToggleDownvote}
/> />
</View> </View>
</View> </View>

View File

@ -48,10 +48,15 @@ export const FeedItem = observer(function FeedItem({
.toggleRepost() .toggleRepost()
.catch(e => console.error('Failed to toggle repost', record, e)) .catch(e => console.error('Failed to toggle repost', record, e))
} }
const onPressToggleLike = () => { const onPressToggleUpvote = () => {
item item
.toggleLike() .toggleUpvote()
.catch(e => console.error('Failed to toggle like', record, e)) .catch(e => console.error('Failed to toggle upvote', record, e))
}
const onPressToggleDownvote = () => {
item
.toggleDownvote()
.catch(e => console.error('Failed to toggle downvote', record, e))
} }
const onPressShare = (uri: string) => { const onPressShare = (uri: string) => {
store.shell.openModal(new SharePostModel(uri)) store.shell.openModal(new SharePostModel(uri))
@ -129,12 +134,15 @@ export const FeedItem = observer(function FeedItem({
<PostCtrls <PostCtrls
replyCount={item.replyCount} replyCount={item.replyCount}
repostCount={item.repostCount} repostCount={item.repostCount}
likeCount={item.likeCount} upvoteCount={item.upvoteCount}
downvoteCount={item.downvoteCount}
isReposted={!!item.myState.repost} isReposted={!!item.myState.repost}
isLiked={!!item.myState.like} isUpvoted={!!item.myState.upvote}
isDownvoted={!!item.myState.downvote}
onPressReply={onPressReply} onPressReply={onPressReply}
onPressToggleRepost={onPressToggleRepost} onPressToggleRepost={onPressToggleRepost}
onPressToggleLike={onPressToggleLike} onPressToggleUpvote={onPressToggleUpvote}
onPressToggleDownvote={onPressToggleDownvote}
/> />
</View> </View>
</View> </View>

View File

@ -7,12 +7,15 @@ import {s, colors} from '../../lib/styles'
interface PostCtrlsOpts { interface PostCtrlsOpts {
replyCount: number replyCount: number
repostCount: number repostCount: number
likeCount: number upvoteCount: number
downvoteCount: number
isReposted: boolean isReposted: boolean
isLiked: boolean isUpvoted: boolean
isDownvoted: boolean
onPressReply: () => void onPressReply: () => void
onPressToggleRepost: () => void onPressToggleRepost: () => void
onPressToggleLike: () => void onPressToggleUpvote: () => void
onPressToggleDownvote: () => void
} }
export function PostCtrls(opts: PostCtrlsOpts) { export function PostCtrls(opts: PostCtrlsOpts) {
@ -36,22 +39,27 @@ export function PostCtrls(opts: PostCtrlsOpts) {
{opts.repostCount} {opts.repostCount}
</Text> </Text>
</TouchableOpacity> </TouchableOpacity>
<TouchableOpacity style={styles.ctrl} onPress={opts.onPressToggleLike}> <TouchableOpacity style={styles.ctrl} onPress={opts.onPressToggleUpvote}>
{opts.isLiked ? ( {opts.isUpvoted ? (
<UpIconSolid style={styles.ctrlIconUpvoted} size={18} /> <UpIconSolid style={styles.ctrlIconUpvoted} size={18} />
) : ( ) : (
<UpIcon style={styles.ctrlIcon} size={18} /> <UpIcon style={styles.ctrlIcon} size={18} />
)} )}
<Text style={opts.isLiked ? [s.bold, s.blue3, s.f13] : s.f13}> <Text style={opts.isUpvoted ? [s.bold, s.red3, s.f13] : s.f13}>
{opts.likeCount} {opts.upvoteCount}
</Text> </Text>
</TouchableOpacity> </TouchableOpacity>
<TouchableOpacity style={styles.ctrl} onPress={opts.onPressToggleLike}> <TouchableOpacity
{opts.isLiked ? ( style={styles.ctrl}
onPress={opts.onPressToggleDownvote}>
{opts.isDownvoted ? (
<DownIconSolid style={styles.ctrlIconDownvoted} size={18} /> <DownIconSolid style={styles.ctrlIconDownvoted} size={18} />
) : ( ) : (
<DownIcon style={styles.ctrlIcon} size={18} /> <DownIcon style={styles.ctrlIcon} size={18} />
)} )}
<Text style={opts.isDownvoted ? [s.bold, s.blue3, s.f13] : s.f13}>
{opts.downvoteCount}
</Text>
</TouchableOpacity> </TouchableOpacity>
</View> </View>
) )
@ -78,10 +86,10 @@ const styles = StyleSheet.create({
}, },
ctrlIconUpvoted: { ctrlIconUpvoted: {
marginRight: 5, marginRight: 5,
color: colors.blue3, color: colors.red3,
}, },
ctrlIconDownvoted: { ctrlIconDownvoted: {
marginRight: 5, marginRight: 5,
color: colors.red3, color: colors.blue3,
}, },
}) })

View File

@ -31,7 +31,7 @@ export function UserInfoText({
useEffect(() => { useEffect(() => {
let aborted = false let aborted = false
// TODO use caching to reduce loads // TODO use caching to reduce loads
store.api.app.bsky.actor.getProfile({user: did}).then( store.api.app.bsky.actor.getProfile({actor: did}).then(
v => { v => {
if (aborted) return if (aborted) return
setProfile(v.data) setProfile(v.data)

View File

@ -7,7 +7,8 @@ import {Search} from './screens/Search'
import {Notifications} from './screens/Notifications' import {Notifications} from './screens/Notifications'
import {NotFound} from './screens/NotFound' import {NotFound} from './screens/NotFound'
import {PostThread} from './screens/PostThread' import {PostThread} from './screens/PostThread'
import {PostLikedBy} from './screens/PostLikedBy' import {PostUpvotedBy} from './screens/PostUpvotedBy'
import {PostDownvotedBy} from './screens/PostDownvotedBy'
import {PostRepostedBy} from './screens/PostRepostedBy' import {PostRepostedBy} from './screens/PostRepostedBy'
import {Profile} from './screens/Profile' import {Profile} from './screens/Profile'
import {ProfileFollowers} from './screens/ProfileFollowers' import {ProfileFollowers} from './screens/ProfileFollowers'
@ -42,9 +43,14 @@ export const routes: Route[] = [
r('/profile/(?<name>[^/]+)/post/(?<rkey>[^/]+)'), r('/profile/(?<name>[^/]+)/post/(?<rkey>[^/]+)'),
], ],
[ [
PostLikedBy, PostUpvotedBy,
'heart', 'heart',
r('/profile/(?<name>[^/]+)/post/(?<rkey>[^/]+)/liked-by'), r('/profile/(?<name>[^/]+)/post/(?<rkey>[^/]+)/upvoted-by'),
],
[
PostDownvotedBy,
'heart',
r('/profile/(?<name>[^/]+)/post/(?<rkey>[^/]+)/downvoted-by'),
], ],
[ [
PostRepostedBy, PostRepostedBy,

View File

@ -0,0 +1,26 @@
import React, {useEffect} from 'react'
import {View} from 'react-native'
import {ViewHeader} from '../com/util/ViewHeader'
import {PostVotedBy as PostLikedByComponent} from '../com/post-thread/PostVotedBy'
import {ScreenParams} from '../routes'
import {useStores} from '../../state'
import {makeRecordUri} from '../lib/strings'
export const PostDownvotedBy = ({visible, params}: ScreenParams) => {
const store = useStores()
const {name, rkey} = params
const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey)
useEffect(() => {
if (visible) {
store.nav.setTitle('Downvoted by')
}
}, [store, visible])
return (
<View>
<ViewHeader title="Downvoted by" />
<PostLikedByComponent uri={uri} direction="down" />
</View>
)
}

View File

@ -1,26 +1,26 @@
import React, {useEffect} from 'react' import React, {useEffect} from 'react'
import {View} from 'react-native' import {View} from 'react-native'
import {ViewHeader} from '../com/util/ViewHeader' import {ViewHeader} from '../com/util/ViewHeader'
import {PostLikedBy as PostLikedByComponent} from '../com/post-thread/PostLikedBy' import {PostVotedBy as PostLikedByComponent} from '../com/post-thread/PostVotedBy'
import {ScreenParams} from '../routes' import {ScreenParams} from '../routes'
import {useStores} from '../../state' import {useStores} from '../../state'
import {makeRecordUri} from '../lib/strings' import {makeRecordUri} from '../lib/strings'
export const PostLikedBy = ({visible, params}: ScreenParams) => { export const PostUpvotedBy = ({visible, params}: ScreenParams) => {
const store = useStores() const store = useStores()
const {name, rkey} = params const {name, rkey} = params
const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey) const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey)
useEffect(() => { useEffect(() => {
if (visible) { if (visible) {
store.nav.setTitle('Liked by') store.nav.setTitle('Upvoted by')
} }
}, [store, visible]) }, [store, visible])
return ( return (
<View> <View>
<ViewHeader title="Liked by" /> <ViewHeader title="Upvoted by" />
<PostLikedByComponent uri={uri} /> <PostLikedByComponent uri={uri} direction="up" />
</View> </View>
) )
} }