mirror of
https://github.com/sky-systems/sky_phone.git
synced 2026-09-04 09:13:24 +00:00
ENH - rebuild Picstagram with SKYUI
Replace the legacy Konsta-based layout at its source with SKYUI components and responsive app-specific styling. Add media creation, profile and social improvements, comment reactions and replies, plus Activity and phone notification delivery with the required database migration.
This commit is contained in:
+12
-2
@@ -205,7 +205,15 @@ type PicstagramVerificationData = {
|
||||
type PicstagramNotificationData = {
|
||||
actor?: string
|
||||
device?: PhoneNotificationDevicePayload
|
||||
kind?: 'like' | 'comment' | 'follow' | 'follow_request' | 'verified'
|
||||
kind?:
|
||||
| 'like'
|
||||
| 'comment'
|
||||
| 'comment_like'
|
||||
| 'reply'
|
||||
| 'follow'
|
||||
| 'follow_request'
|
||||
| 'request_accepted'
|
||||
| 'verified'
|
||||
postId?: string
|
||||
text?: string
|
||||
title?: string
|
||||
@@ -1411,7 +1419,9 @@ onBeforeUnmount(() => {
|
||||
<Transition :name="appTransitionName">
|
||||
<component
|
||||
:is="Component"
|
||||
v-if="!setupRequired && (isAppRoute || isDevelopmentRoute)"
|
||||
v-if="
|
||||
!setupRequired && (isAppRoute || isDevelopmentRoute)
|
||||
"
|
||||
:key="
|
||||
isDevelopmentRoute ? String(route.name) : route.path
|
||||
"
|
||||
|
||||
@@ -988,8 +988,12 @@ const defaultLocales: LocaleTree = {
|
||||
story: 'Story',
|
||||
choosePhotos: 'Choose photos',
|
||||
choosePhoto: 'Choose photo',
|
||||
choosePhotosHint: 'Select up to five photos from Photos',
|
||||
chooseStoryHint: 'Select one photo from Photos',
|
||||
photo: 'Photo',
|
||||
video: 'Video',
|
||||
gallery: 'Gallery',
|
||||
camera: 'Camera',
|
||||
choosePhotosHint: 'Select up to five photos or one video',
|
||||
chooseStoryHint: 'Select one photo or video',
|
||||
selectedPhotos: '{count} selected',
|
||||
changePhotos: 'Change selection',
|
||||
caption: 'Caption',
|
||||
@@ -1006,6 +1010,8 @@ const defaultLocales: LocaleTree = {
|
||||
noComments: 'No comments yet',
|
||||
addComment: 'Add a comment...',
|
||||
reply: 'Reply',
|
||||
replyPlaceholder: 'Write a reply...',
|
||||
replyingTo: 'Replying to {handle}',
|
||||
removeComment: 'Remove comment',
|
||||
likes: '{count} likes',
|
||||
viewComments: 'View all {count} comments',
|
||||
@@ -1026,6 +1032,8 @@ const defaultLocales: LocaleTree = {
|
||||
unfollow: 'Following',
|
||||
accept: 'Accept',
|
||||
decline: 'Decline',
|
||||
noConnections: 'No profiles here yet',
|
||||
shareProfile: 'Share profile',
|
||||
followRequests: 'Follow requests',
|
||||
noActivity: 'No activity yet',
|
||||
markRead: 'Mark as read',
|
||||
@@ -1079,6 +1087,8 @@ const defaultLocales: LocaleTree = {
|
||||
request_accepted: 'accepted your follow request',
|
||||
like: 'liked your post',
|
||||
comment: 'commented on your post',
|
||||
comment_like: 'liked your comment',
|
||||
reply: 'replied to your comment',
|
||||
verified: 'verification changed',
|
||||
},
|
||||
notifications: {
|
||||
@@ -1087,6 +1097,8 @@ const defaultLocales: LocaleTree = {
|
||||
request_accepted: '{actor} accepted your follow request.',
|
||||
like: '{actor} liked your post.',
|
||||
comment: '{actor} commented on your post.',
|
||||
comment_like: '{actor} liked your comment.',
|
||||
reply: '{actor} replied to your comment.',
|
||||
verified: 'Your Picstagram profile is now verified.',
|
||||
default: 'You have new Picstagram activity.',
|
||||
},
|
||||
@@ -3913,15 +3925,19 @@ const defaultLocales: LocaleTree = {
|
||||
eraseLocalApps: 'Downloaded apps and local app sessions',
|
||||
keptSafe: 'Kept Safe',
|
||||
keepSimData: 'SIM Card & Phone Number',
|
||||
keepSimDataBody: 'Your number and SIM-backed communication remain on the SIM.',
|
||||
keepSimDataBody:
|
||||
'Your number and SIM-backed communication remain on the SIM.',
|
||||
keepCloudData: 'Account & Cloud Data',
|
||||
keepCloudDataBody: 'Supported information remains in its app or Sky Cloud account.',
|
||||
resetSetupAssistant: 'When erasing is complete, Setup Assistant starts automatically.',
|
||||
keepCloudDataBody:
|
||||
'Supported information remains in its app or Sky Cloud account.',
|
||||
resetSetupAssistant:
|
||||
'When erasing is complete, Setup Assistant starts automatically.',
|
||||
factoryReset: 'Erase All Content and Settings',
|
||||
factoryResetBody:
|
||||
'This cannot be undone. Local settings, apps and unsynced content are erased. Your SIM number and account-backed data remain available.',
|
||||
factoryResetProgress: 'Erasing Sky Phone',
|
||||
factoryResetWarning: 'Keep this phone open. Your SIM and cloud data are safe.',
|
||||
factoryResetWarning:
|
||||
'Keep this phone open. Your SIM and cloud data are safe.',
|
||||
factoryResetSystemProcess: 'Secure system process',
|
||||
factoryResetPreparing: 'Preparing secure reset',
|
||||
factoryResetPreparingDetail: 'Checking device data',
|
||||
@@ -4084,8 +4100,10 @@ const defaultLocales: LocaleTree = {
|
||||
create: 'Create Account',
|
||||
signInTitle: 'Sign in to Sky Cloud',
|
||||
createTitle: 'Create your Sky Cloud account',
|
||||
signInBody: 'Access your protected data, purchases and settings on this phone.',
|
||||
createBody: 'Choose your personal iFruit address and keep important data available across devices.',
|
||||
signInBody:
|
||||
'Access your protected data, purchases and settings on this phone.',
|
||||
createBody:
|
||||
'Choose your personal iFruit address and keep important data available across devices.',
|
||||
accountPreview: 'Your Sky Cloud ID',
|
||||
accountName: 'Account name',
|
||||
addressPlaceholder: 'alex.morgan',
|
||||
@@ -4116,7 +4134,8 @@ const defaultLocales: LocaleTree = {
|
||||
eyebrow: 'Privacy & Security',
|
||||
title: 'Protect this phone',
|
||||
body: 'Choose a four- or six-digit passcode to keep your apps and local information private.',
|
||||
local: 'The passcode belongs to this physical phone and is never transferred with the SIM.',
|
||||
local:
|
||||
'The passcode belongs to this physical phone and is never transferred with the SIM.',
|
||||
create: 'Create Passcode',
|
||||
createSelected: 'Create {count}-Digit Passcode',
|
||||
lengthTitle: 'Choose passcode length',
|
||||
|
||||
@@ -49,7 +49,14 @@ const post: PicstagramPost = {
|
||||
is_saved: false,
|
||||
like_count: 2,
|
||||
location: 'Downtown',
|
||||
media: [{ id: 1, position: 0, url: 'https://example.com/photo.webp' }],
|
||||
media: [
|
||||
{
|
||||
id: 1,
|
||||
media_type: 'photo',
|
||||
position: 0,
|
||||
url: 'https://example.com/photo.webp',
|
||||
},
|
||||
],
|
||||
private: false,
|
||||
profile_id: profile.id,
|
||||
verified: false,
|
||||
@@ -62,9 +69,12 @@ const comment: PicstagramComment = {
|
||||
display_name: 'Nova',
|
||||
handle: 'nova',
|
||||
id: 'comment-1',
|
||||
is_liked: false,
|
||||
is_owner: true,
|
||||
like_count: 0,
|
||||
parent_id: null,
|
||||
profile_id: profile.id,
|
||||
reply_to_handle: null,
|
||||
verified: false,
|
||||
}
|
||||
|
||||
@@ -91,6 +101,7 @@ const story: PicstagramStory = {
|
||||
handle: 'nova',
|
||||
id: 'story-1',
|
||||
is_owner: true,
|
||||
media_type: 'photo',
|
||||
profile_id: profile.id,
|
||||
seen: true,
|
||||
url: 'https://example.com/story.webp',
|
||||
|
||||
@@ -29,6 +29,7 @@ export const usePicstagramStore = defineStore('picstagram', {
|
||||
activities: [] as PicstagramActivity[],
|
||||
authenticated: false,
|
||||
comments: [] as PicstagramComment[],
|
||||
connections: [] as PicstagramProfile[],
|
||||
explore: [] as PicstagramPost[],
|
||||
exploreCursor: null as string | null,
|
||||
feed: [] as PicstagramPost[],
|
||||
@@ -199,6 +200,7 @@ export const usePicstagramStore = defineStore('picstagram', {
|
||||
commentsEnabled: boolean
|
||||
location: string
|
||||
mediaIds: number[]
|
||||
mediaType: 'photo' | 'video'
|
||||
}): Promise<NuiResponse<{ id: string }>> {
|
||||
const response = await nuiCall<{ id: string }>(
|
||||
'picstagram:publish-post',
|
||||
@@ -283,8 +285,9 @@ export const usePicstagramStore = defineStore('picstagram', {
|
||||
id: string,
|
||||
body: string,
|
||||
parentId?: string,
|
||||
replyToId?: string,
|
||||
): Promise<NuiResponse> {
|
||||
return nuiCall('picstagram:comment', { body, id, parentId })
|
||||
return nuiCall('picstagram:comment', { body, id, parentId, replyToId })
|
||||
},
|
||||
async removeComment(id: string): Promise<boolean> {
|
||||
const response = await nuiCall('picstagram:remove-comment', { id })
|
||||
@@ -292,6 +295,29 @@ export const usePicstagramStore = defineStore('picstagram', {
|
||||
this.comments = this.comments.filter((comment) => comment.id !== id)
|
||||
return response.success
|
||||
},
|
||||
async reactComment(comment: PicstagramComment): Promise<void> {
|
||||
const active = !comment.is_liked
|
||||
comment.is_liked = active
|
||||
comment.like_count += active ? 1 : -1
|
||||
const response = await nuiCall('picstagram:comment-react', {
|
||||
active,
|
||||
id: comment.id,
|
||||
})
|
||||
if (response.success) return
|
||||
comment.is_liked = !active
|
||||
comment.like_count += active ? -1 : 1
|
||||
},
|
||||
async loadConnections(
|
||||
profileId: string,
|
||||
mode: 'followers' | 'following',
|
||||
): Promise<boolean> {
|
||||
const response = await nuiCall<PicstagramProfile[]>(
|
||||
'picstagram:connections',
|
||||
{ mode, profileId },
|
||||
)
|
||||
this.connections = response.success && response.data ? response.data : []
|
||||
return response.success
|
||||
},
|
||||
async loadStories(): Promise<boolean> {
|
||||
const response = await nuiCall<PicstagramStory[]>('picstagram:stories')
|
||||
this.stories = response.success && response.data ? response.data : []
|
||||
@@ -300,10 +326,11 @@ export const usePicstagramStore = defineStore('picstagram', {
|
||||
async publishStory(
|
||||
mediaId: number,
|
||||
body: string,
|
||||
mediaType: 'photo' | 'video',
|
||||
): Promise<NuiResponse<{ id: string }>> {
|
||||
const response = await nuiCall<{ id: string }>(
|
||||
'picstagram:publish-story',
|
||||
{ body, mediaId },
|
||||
{ body, mediaId, mediaType },
|
||||
)
|
||||
if (response.success) await this.loadStories()
|
||||
return response
|
||||
|
||||
@@ -20,6 +20,7 @@ export type PicstagramProfile = {
|
||||
|
||||
export type PicstagramPostMedia = {
|
||||
id: number
|
||||
media_type: 'photo' | 'video'
|
||||
position: number
|
||||
url: string
|
||||
}
|
||||
@@ -67,9 +68,12 @@ export type PicstagramComment = {
|
||||
display_name: string
|
||||
handle: string
|
||||
id: string
|
||||
is_liked: boolean
|
||||
is_owner: boolean
|
||||
like_count: number
|
||||
parent_id: string | null
|
||||
profile_id: string
|
||||
reply_to_handle: string | null
|
||||
verified: boolean
|
||||
}
|
||||
|
||||
@@ -82,6 +86,7 @@ export type PicstagramStory = {
|
||||
handle: string
|
||||
id: string
|
||||
is_owner: boolean
|
||||
media_type: 'photo' | 'video'
|
||||
profile_id: string
|
||||
seen: boolean
|
||||
url: string
|
||||
@@ -104,6 +109,8 @@ export type PicstagramActivityKind =
|
||||
| 'request_accepted'
|
||||
| 'like'
|
||||
| 'comment'
|
||||
| 'comment_like'
|
||||
| 'reply'
|
||||
| 'verified'
|
||||
|
||||
export type PicstagramActivity = {
|
||||
|
||||
+2641
-2005
File diff suppressed because it is too large
Load Diff
@@ -716,11 +716,13 @@ let picstagramPosts = [
|
||||
media: [
|
||||
{
|
||||
id: 8101,
|
||||
media_type: 'photo',
|
||||
position: 0,
|
||||
url: 'https://images.unsplash.com/photo-1518005020951-eccb494ad742?auto=format&fit=crop&w=900&q=85',
|
||||
},
|
||||
{
|
||||
id: 8102,
|
||||
media_type: 'photo',
|
||||
position: 1,
|
||||
url: 'https://images.unsplash.com/photo-1444723121867-7a241cacace9?auto=format&fit=crop&w=900&q=85',
|
||||
},
|
||||
@@ -746,6 +748,7 @@ let picstagramPosts = [
|
||||
media: [
|
||||
{
|
||||
id: 8103,
|
||||
media_type: 'photo',
|
||||
position: 0,
|
||||
url: 'https://images.unsplash.com/photo-1519608487953-e999c86e7455?auto=format&fit=crop&w=900&q=85',
|
||||
},
|
||||
@@ -763,9 +766,12 @@ let picstagramComments = [
|
||||
display_name: picstagramProfiles[1].display_name,
|
||||
handle: picstagramProfiles[1].handle,
|
||||
id: 'pic-comment-1',
|
||||
is_liked: false,
|
||||
is_owner: false,
|
||||
like_count: 4,
|
||||
parent_id: null,
|
||||
profile_id: picstagramProfiles[1].id,
|
||||
reply_to_handle: null,
|
||||
verified: true,
|
||||
},
|
||||
]
|
||||
@@ -779,6 +785,7 @@ let picstagramStories = [
|
||||
handle: picstagramProfiles[1].handle,
|
||||
id: 'pic-story-1',
|
||||
is_owner: false,
|
||||
media_type: 'photo',
|
||||
profile_id: picstagramProfiles[1].id,
|
||||
seen: false,
|
||||
url: 'https://images.unsplash.com/photo-1500530855697-b586d89ba3ee?auto=format&fit=crop&w=900&q=85',
|
||||
@@ -2490,7 +2497,12 @@ let mockMedia = [
|
||||
favorite: true,
|
||||
id: 2,
|
||||
mediaType: 'video',
|
||||
thumbnailUrl: mockGalleryImage('Flower Video', '#7f1d1d', '#365314', '#fb7185'),
|
||||
thumbnailUrl: mockGalleryImage(
|
||||
'Flower Video',
|
||||
'#7f1d1d',
|
||||
'#365314',
|
||||
'#fb7185',
|
||||
),
|
||||
url: 'https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4',
|
||||
},
|
||||
{
|
||||
@@ -2519,7 +2531,12 @@ let mockMedia = [
|
||||
favorite: false,
|
||||
id: 6,
|
||||
mediaType: 'video',
|
||||
thumbnailUrl: mockGalleryImage('Sintel Video', '#9a3412', '#431407', '#fdba74'),
|
||||
thumbnailUrl: mockGalleryImage(
|
||||
'Sintel Video',
|
||||
'#9a3412',
|
||||
'#431407',
|
||||
'#fdba74',
|
||||
),
|
||||
url: 'https://media.w3.org/2010/05/sintel/trailer.mp4',
|
||||
},
|
||||
{
|
||||
@@ -2548,7 +2565,12 @@ let mockMedia = [
|
||||
favorite: false,
|
||||
id: 10,
|
||||
mediaType: 'video',
|
||||
thumbnailUrl: mockGalleryImage('Bunny Video', '#166534', '#14532d', '#86efac'),
|
||||
thumbnailUrl: mockGalleryImage(
|
||||
'Bunny Video',
|
||||
'#166534',
|
||||
'#14532d',
|
||||
'#86efac',
|
||||
),
|
||||
url: 'https://www.w3schools.com/html/mov_bbb.mp4',
|
||||
},
|
||||
{
|
||||
@@ -6308,6 +6330,14 @@ app.post('/api/:endpoint', (request, response) => {
|
||||
})
|
||||
return
|
||||
}
|
||||
if (endpoint === 'picstagram:connections') {
|
||||
const targetId = request.body.profileId
|
||||
const profiles = picstagramProfiles.filter(
|
||||
(profile) => profile.id !== targetId,
|
||||
)
|
||||
response.json({ success: true, data: profiles })
|
||||
return
|
||||
}
|
||||
if (endpoint === 'picstagram:update-profile') {
|
||||
Object.assign(picstagramProfiles[0], {
|
||||
bio: request.body.bio,
|
||||
@@ -6321,7 +6351,7 @@ app.post('/api/:endpoint', (request, response) => {
|
||||
if (endpoint === 'picstagram:publish-post') {
|
||||
const media = request.body.mediaIds
|
||||
.map((id) => mockMedia.find((item) => item.id === id))
|
||||
.filter((item) => item?.mediaType === 'photo')
|
||||
.filter((item) => item?.mediaType === request.body.mediaType)
|
||||
if (!media.length) {
|
||||
response.json({ success: false, error: 'invalid_media' })
|
||||
return
|
||||
@@ -6342,6 +6372,7 @@ app.post('/api/:endpoint', (request, response) => {
|
||||
location: request.body.location,
|
||||
media: media.map((item, position) => ({
|
||||
id: item.id,
|
||||
media_type: item.mediaType,
|
||||
position,
|
||||
url: item.url,
|
||||
})),
|
||||
@@ -6423,6 +6454,9 @@ app.post('/api/:endpoint', (request, response) => {
|
||||
return
|
||||
}
|
||||
if (endpoint === 'picstagram:comment') {
|
||||
const replyTarget = request.body.replyToId
|
||||
? picstagramComments.find((entry) => entry.id === request.body.replyToId)
|
||||
: null
|
||||
const comment = {
|
||||
avatar_url: picstagramProfiles[0].avatar_url,
|
||||
body: request.body.body,
|
||||
@@ -6430,15 +6464,43 @@ app.post('/api/:endpoint', (request, response) => {
|
||||
display_name: picstagramProfiles[0].display_name,
|
||||
handle: picstagramProfiles[0].handle,
|
||||
id: `pic-comment-${Date.now()}`,
|
||||
is_liked: false,
|
||||
is_owner: true,
|
||||
parent_id: request.body.parentId ?? null,
|
||||
like_count: 0,
|
||||
parent_id:
|
||||
replyTarget?.parent_id ??
|
||||
replyTarget?.id ??
|
||||
request.body.parentId ??
|
||||
null,
|
||||
profile_id: picstagramProfiles[0].id,
|
||||
reply_to_handle: replyTarget
|
||||
? replyTarget.handle
|
||||
: request.body.parentId
|
||||
? (picstagramComments.find(
|
||||
(entry) => entry.id === request.body.parentId,
|
||||
)?.handle ?? null)
|
||||
: null,
|
||||
verified: picstagramProfiles[0].verified,
|
||||
}
|
||||
picstagramComments.push(comment)
|
||||
response.json({ success: true, data: { id: comment.id } })
|
||||
return
|
||||
}
|
||||
if (endpoint === 'picstagram:comment-react') {
|
||||
const comment = picstagramComments.find(
|
||||
(item) => item.id === request.body.id,
|
||||
)
|
||||
if (comment) {
|
||||
const changed = comment.is_liked !== request.body.active
|
||||
comment.is_liked = request.body.active
|
||||
if (changed) comment.like_count += request.body.active ? 1 : -1
|
||||
}
|
||||
response.json({
|
||||
success: Boolean(comment),
|
||||
error: comment ? undefined : 'invalid_comment',
|
||||
})
|
||||
return
|
||||
}
|
||||
if (endpoint === 'picstagram:remove-comment') {
|
||||
picstagramComments = picstagramComments.filter(
|
||||
(comment) => comment.id !== request.body.id,
|
||||
@@ -6452,7 +6514,7 @@ app.post('/api/:endpoint', (request, response) => {
|
||||
}
|
||||
if (endpoint === 'picstagram:publish-story') {
|
||||
const media = mockMedia.find((item) => item.id === request.body.mediaId)
|
||||
if (!media || media.mediaType !== 'photo') {
|
||||
if (!media || media.mediaType !== request.body.mediaType) {
|
||||
response.json({ success: false, error: 'invalid_media' })
|
||||
return
|
||||
}
|
||||
@@ -6465,6 +6527,7 @@ app.post('/api/:endpoint', (request, response) => {
|
||||
handle: picstagramProfiles[0].handle,
|
||||
id: `pic-story-${Date.now()}`,
|
||||
is_owner: true,
|
||||
media_type: media.mediaType,
|
||||
profile_id: picstagramProfiles[0].id,
|
||||
seen: true,
|
||||
url: media.url,
|
||||
|
||||
@@ -415,20 +415,20 @@ Locales["en"] = {
|
||||
discoverPeople = "Discover people", searchPlaceholder = "Search profiles, captions, places, or tags",
|
||||
noResults = "No results", noResultsBody = "Try another username, caption, place, or hashtag.",
|
||||
newPost = "New Post", newStory = "New Story", post = "Post", story = "Story",
|
||||
choosePhotos = "Choose photos", choosePhoto = "Choose photo",
|
||||
choosePhotosHint = "Select up to five photos from Photos", chooseStoryHint = "Select one photo from Photos",
|
||||
choosePhotos = "Choose photos", choosePhoto = "Choose photo", photo = "Photo", video = "Video", gallery = "Gallery", camera = "Camera",
|
||||
choosePhotosHint = "Select up to five photos or one video", chooseStoryHint = "Select one photo or video",
|
||||
selectedPhotos = "{count} selected", changePhotos = "Change selection", caption = "Caption",
|
||||
captionPlaceholder = "Write a caption...", storyTextPlaceholder = "Add a short story text...",
|
||||
location = "Location", locationPlaceholder = "Add a place", allowComments = "Allow comments",
|
||||
publishing = "Publishing...", published = "Your post is live.", storyPublished = "Your story is live.",
|
||||
comments = "Comments", comment = "Comment", noComments = "No comments yet", addComment = "Add a comment...",
|
||||
reply = "Reply", removeComment = "Remove comment", likes = "{count} likes",
|
||||
reply = "Reply", replyPlaceholder = "Write a reply...", replyingTo = "Replying to {handle}", removeComment = "Remove comment", likes = "{count} likes",
|
||||
viewComments = "View all {count} comments", like = "Like", unlike = "Unlike", save = "Save",
|
||||
unsave = "Remove from saved", more = "More", archive = "Archive", restore = "Restore",
|
||||
deletePost = "Delete post", deletePostTitle = "Delete this post?",
|
||||
deletePostBody = "The post and its reactions will no longer be available.",
|
||||
privateProfile = "This profile is private", privateProfileBody = "Follow this profile to see its posts and stories.",
|
||||
follow = "Follow", requested = "Requested", unfollow = "Following", accept = "Accept", decline = "Decline",
|
||||
follow = "Follow", requested = "Requested", unfollow = "Following", accept = "Accept", decline = "Decline", noConnections = "No profiles here yet", shareProfile = "Share profile",
|
||||
followRequests = "Follow requests", noActivity = "No activity yet", markRead = "Mark as read",
|
||||
publicProfile = "Public profile", privateProfileSetting = "Private profile",
|
||||
privacyHint = "Only accepted followers can see your posts and stories.", bio = "Bio",
|
||||
@@ -442,8 +442,8 @@ Locales["en"] = {
|
||||
noDetails = "No additional details", hide = "Hide", remove = "Remove", restoreAction = "Restore", dismiss = "Dismiss",
|
||||
reportTargets = { post = "Post", profile = "Profile", comment = "Comment", story = "Story" },
|
||||
reportReasons = { spam = "Spam or misleading", harassment = "Harassment or bullying", dangerous = "Dangerous activity", illegal = "Illegal content", other = "Something else" },
|
||||
activityKinds = { follow_request = "requested to follow you", follow = "started following you", request_accepted = "accepted your follow request", like = "liked your post", comment = "commented on your post", verified = "verification changed" },
|
||||
notifications = { follow_request = "{actor} requested to follow you.", follow = "{actor} started following you.", request_accepted = "{actor} accepted your follow request.", like = "{actor} liked your post.", comment = "{actor} commented on your post.", verified = "Your Picstagram profile is now verified.", default = "You have new Picstagram activity." },
|
||||
activityKinds = { follow_request = "requested to follow you", follow = "started following you", request_accepted = "accepted your follow request", like = "liked your post", comment = "commented on your post", comment_like = "liked your comment", reply = "replied to your comment", verified = "verification changed" },
|
||||
notifications = { follow_request = "{actor} requested to follow you.", follow = "{actor} started following you.", request_accepted = "{actor} accepted your follow request.", like = "{actor} liked your post.", comment = "{actor} commented on your post.", comment_like = "{actor} liked your comment.", reply = "{actor} replied to your comment.", verified = "Your Picstagram profile is now verified.", default = "You have new Picstagram activity." },
|
||||
errors = {
|
||||
invalid_post = "Check the post details.", invalid_story = "Check the story details.",
|
||||
invalid_media = "Choose photos owned by this phone.", invalid_comment = "Enter a valid comment.",
|
||||
|
||||
@@ -120,6 +120,7 @@ local server_callbacks = {
|
||||
"picstagram:search",
|
||||
"picstagram:saved",
|
||||
"picstagram:profile",
|
||||
"picstagram:connections",
|
||||
"picstagram:update-profile",
|
||||
"picstagram:publish-post",
|
||||
"picstagram:update-post",
|
||||
@@ -129,6 +130,7 @@ local server_callbacks = {
|
||||
"picstagram:respond-follow",
|
||||
"picstagram:comments",
|
||||
"picstagram:comment",
|
||||
"picstagram:comment-react",
|
||||
"picstagram:remove-comment",
|
||||
"picstagram:publish-story",
|
||||
"picstagram:stories",
|
||||
|
||||
@@ -1399,6 +1399,24 @@ local schema = {
|
||||
},
|
||||
tableOptions = "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
|
||||
},
|
||||
{
|
||||
name = "sky_phone_picstagram_comment_reactions",
|
||||
columns = {
|
||||
{ name = "id", type = "BIGINT UNSIGNED NOT NULL AUTO_INCREMENT" },
|
||||
{ name = "comment_id", type = "CHAR(36) NOT NULL", characterSet = "ascii", collation = "ascii_bin" },
|
||||
{ name = "profile_id", type = "CHAR(36) NOT NULL", characterSet = "ascii", collation = "ascii_bin" },
|
||||
{ name = "created_at", type = "DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP" },
|
||||
},
|
||||
primaryKey = "id",
|
||||
uniqueKeys = {
|
||||
{ name = "uniq_sky_phone_picstagram_comment_reaction", columns = "(`comment_id`, `profile_id`)" },
|
||||
},
|
||||
foreignKeys = {
|
||||
{ column = "comment_id", references = "`sky_phone_picstagram_comments` (`id`) ON DELETE CASCADE" },
|
||||
{ column = "profile_id", references = "`sky_phone_picstagram_profiles` (`id`) ON DELETE CASCADE" },
|
||||
},
|
||||
tableOptions = "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
|
||||
},
|
||||
{
|
||||
name = "sky_phone_picstagram_stories",
|
||||
columns = {
|
||||
@@ -1446,7 +1464,7 @@ local schema = {
|
||||
{ name = "recipient_id", type = "CHAR(36) NOT NULL", characterSet = "ascii", collation = "ascii_bin" },
|
||||
{ name = "actor_id", type = "CHAR(36) NOT NULL", characterSet = "ascii", collation = "ascii_bin" },
|
||||
{ name = "post_id", type = "CHAR(36) NULL", characterSet = "ascii", collation = "ascii_bin" },
|
||||
{ name = "kind", type = "ENUM('follow_request', 'follow', 'request_accepted', 'like', 'comment', 'verified') NOT NULL" },
|
||||
{ name = "kind", type = "ENUM('follow_request', 'follow', 'request_accepted', 'like', 'comment', 'comment_like', 'reply', 'verified') NOT NULL" },
|
||||
{ name = "read_at", type = "DATETIME NULL" },
|
||||
{ name = "created_at", type = "DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP" },
|
||||
},
|
||||
@@ -2621,6 +2639,10 @@ Bridge.Database.Query([[
|
||||
ALTER TABLE `sky_phone_feather_notifications`
|
||||
MODIFY COLUMN `kind` ENUM('like', 'reply', 'follow', 'quote') NOT NULL
|
||||
]], {})
|
||||
Bridge.Database.Query([[
|
||||
ALTER TABLE `sky_phone_picstagram_activities`
|
||||
MODIFY COLUMN `kind` ENUM('follow_request', 'follow', 'request_accepted', 'like', 'comment', 'comment_like', 'reply', 'verified') NOT NULL
|
||||
]], {})
|
||||
Bridge.Database.Query([[
|
||||
ALTER TABLE `sky_phone_flare_swipes`
|
||||
MODIFY COLUMN `choice` ENUM('like', 'pass', 'superlike') NOT NULL
|
||||
|
||||
@@ -154,7 +154,7 @@ local function load_post_media(posts)
|
||||
by_id[post.id] = post
|
||||
end
|
||||
local rows = Bridge.Database.Query(([[
|
||||
SELECT pm.`post_id`, pm.`position`, m.`id`, m.`url`
|
||||
SELECT pm.`post_id`, pm.`position`, m.`id`, m.`url`, m.`media_type`
|
||||
FROM `sky_phone_picstagram_post_media` pm
|
||||
JOIN `sky_phone_media` m ON m.`id` = pm.`media_id`
|
||||
WHERE pm.`post_id` IN (%s)
|
||||
@@ -166,6 +166,7 @@ local function load_post_media(posts)
|
||||
if post then
|
||||
post.media[#post.media + 1] = {
|
||||
id = tonumber(media.id),
|
||||
media_type = media.media_type,
|
||||
position = tonumber(media.position),
|
||||
url = media.url,
|
||||
}
|
||||
@@ -599,6 +600,48 @@ Bridge.Callbacks.Register("sky_phone:picstagram:profile", function(source, data)
|
||||
return { success = true, data = { profile = target, posts = posts } }
|
||||
end)
|
||||
|
||||
Bridge.Callbacks.Register("sky_phone:picstagram:connections", function(source, data)
|
||||
local viewer, error_response = profile_for_session(source)
|
||||
if not viewer then
|
||||
return error_response
|
||||
end
|
||||
local target_id = type(data) == "table" and data.profileId or nil
|
||||
local mode = type(data) == "table" and data.mode or nil
|
||||
if not valid_id(target_id) or (mode ~= "followers" and mode ~= "following") then
|
||||
return { success = false, error = "invalid_request" }
|
||||
end
|
||||
if target_id ~= viewer.id and are_profiles_blocked(viewer.id, target_id) then
|
||||
return { success = false, error = "profile_not_found" }
|
||||
end
|
||||
local join_column = mode == "followers" and "follow.`follower_id`" or "follow.`following_id`"
|
||||
local filter_column = mode == "followers" and "follow.`following_id`" or "follow.`follower_id`"
|
||||
local rows = Bridge.Database.Query(([[
|
||||
SELECT candidate.`id`, candidate.`handle`, candidate.`display_name`, candidate.`bio`,
|
||||
candidate.`avatar_media_id`, candidate.`private`, candidate.`verified`, candidate.`status`,
|
||||
avatar.`url` AS `avatar_url`,
|
||||
COALESCE((SELECT own_follow.`status` FROM `sky_phone_picstagram_follows` own_follow
|
||||
WHERE own_follow.`follower_id` = ? AND own_follow.`following_id` = candidate.`id` LIMIT 1), '') AS `follow_status`,
|
||||
(SELECT COUNT(*) FROM `sky_phone_picstagram_follows` followers
|
||||
WHERE followers.`following_id` = candidate.`id` AND followers.`status` = 'accepted') AS `followers`,
|
||||
(SELECT COUNT(*) FROM `sky_phone_picstagram_follows` following
|
||||
WHERE following.`follower_id` = candidate.`id` AND following.`status` = 'accepted') AS `following`,
|
||||
(SELECT COUNT(*) FROM `sky_phone_picstagram_posts` post
|
||||
WHERE post.`profile_id` = candidate.`id` AND post.`status` = 'published') AS `post_count`
|
||||
FROM `sky_phone_picstagram_follows` follow
|
||||
JOIN `sky_phone_picstagram_profiles` candidate ON candidate.`id` = %s
|
||||
LEFT JOIN `sky_phone_media` avatar ON avatar.`id` = candidate.`avatar_media_id`
|
||||
WHERE %s = ? AND follow.`status` = 'accepted' AND candidate.`status` = 'active'
|
||||
AND NOT EXISTS(SELECT 1 FROM `sky_phone_picstagram_blocks` block
|
||||
WHERE (block.`blocker_id` = ? AND block.`blocked_id` = candidate.`id`)
|
||||
OR (block.`blocked_id` = ? AND block.`blocker_id` = candidate.`id`))
|
||||
ORDER BY candidate.`display_name`, candidate.`handle` LIMIT 200
|
||||
]]):format(join_column, filter_column), { viewer.id, target_id, viewer.id, viewer.id })
|
||||
for index = 1, #rows do
|
||||
hydrate_profile(rows[index], viewer.id)
|
||||
end
|
||||
return { success = true, data = rows }
|
||||
end)
|
||||
|
||||
Bridge.Callbacks.Register("sky_phone:picstagram:update-profile", function(source, data)
|
||||
local profile, error_response = profile_for_session(source)
|
||||
if not profile then
|
||||
@@ -642,7 +685,9 @@ Bridge.Callbacks.Register("sky_phone:picstagram:publish-post", function(source,
|
||||
if not profile then
|
||||
return error_response
|
||||
end
|
||||
if type(data) ~= "table" or type(data.mediaIds) ~= "table" or type(data.commentsEnabled) ~= "boolean" then
|
||||
if type(data) ~= "table" or type(data.mediaIds) ~= "table" or type(data.commentsEnabled) ~= "boolean"
|
||||
or (data.mediaType ~= "photo" and data.mediaType ~= "video")
|
||||
then
|
||||
return { success = false, error = "invalid_post" }
|
||||
end
|
||||
local caption = trim(data.caption) or ""
|
||||
@@ -651,6 +696,7 @@ Bridge.Callbacks.Register("sky_phone:picstagram:publish-post", function(source,
|
||||
or not valid_text(location, 0, Config.Picstagram.LocationMaxLength)
|
||||
or #data.mediaIds < 1
|
||||
or #data.mediaIds > Config.Picstagram.MaxPostMedia
|
||||
or (data.mediaType == "video" and #data.mediaIds ~= 1)
|
||||
then
|
||||
return { success = false, error = "invalid_post" }
|
||||
end
|
||||
@@ -661,7 +707,7 @@ Bridge.Callbacks.Register("sky_phone:picstagram:publish-post", function(source,
|
||||
if not media_id or media_id < 1 or media_id ~= math.floor(media_id) or seen[media_id] then
|
||||
return { success = false, error = "invalid_media" }
|
||||
end
|
||||
if not SkyPhoneMedia.ResolveOwnedMedia(source, tostring(media_id), "photo") then
|
||||
if not SkyPhoneMedia.ResolveOwnedMedia(source, tostring(media_id), data.mediaType) then
|
||||
return { success = false, error = "invalid_media" }
|
||||
end
|
||||
seen[media_id] = true
|
||||
@@ -864,25 +910,75 @@ Bridge.Callbacks.Register("sky_phone:picstagram:comments", function(source, data
|
||||
SELECT comment.`id`, comment.`profile_id`, comment.`parent_id`, comment.`body`,
|
||||
UNIX_TIMESTAMP(comment.`created_at`) AS `created_at_unix`, author.`handle`,
|
||||
author.`display_name`, author.`verified`, avatar.`url` AS `avatar_url`,
|
||||
(comment.`profile_id` = ?) AS `is_owner`
|
||||
parent_author.`handle` AS `reply_to_handle`,
|
||||
(comment.`profile_id` = ?) AS `is_owner`,
|
||||
EXISTS(SELECT 1 FROM `sky_phone_picstagram_comment_reactions` reaction
|
||||
WHERE reaction.`comment_id` = comment.`id` AND reaction.`profile_id` = ?) AS `is_liked`,
|
||||
(SELECT COUNT(*) FROM `sky_phone_picstagram_comment_reactions` reaction
|
||||
WHERE reaction.`comment_id` = comment.`id`) AS `like_count`
|
||||
FROM `sky_phone_picstagram_comments` comment
|
||||
JOIN `sky_phone_picstagram_profiles` author ON author.`id` = comment.`profile_id`
|
||||
LEFT JOIN `sky_phone_media` avatar ON avatar.`id` = author.`avatar_media_id`
|
||||
LEFT JOIN `sky_phone_picstagram_comments` parent ON parent.`id` = comment.`parent_id`
|
||||
LEFT JOIN `sky_phone_picstagram_profiles` parent_author ON parent_author.`id` = parent.`profile_id`
|
||||
WHERE comment.`post_id` = ? AND comment.`status` = 'visible' AND author.`status` = 'active'
|
||||
AND NOT EXISTS(SELECT 1 FROM `sky_phone_picstagram_blocks` block
|
||||
WHERE (block.`blocker_id` = ? AND block.`blocked_id` = comment.`profile_id`)
|
||||
OR (block.`blocked_id` = ? AND block.`blocker_id` = comment.`profile_id`))
|
||||
ORDER BY comment.`created_at`, comment.`id` LIMIT ?
|
||||
]], { profile.id, data.id, profile.id, profile.id, Config.Picstagram.CommentPageSize })
|
||||
ORDER BY COALESCE(parent.`created_at`, comment.`created_at`),
|
||||
comment.`parent_id` IS NOT NULL, comment.`created_at`, comment.`id` LIMIT ?
|
||||
]], { profile.id, profile.id, data.id, profile.id, profile.id, Config.Picstagram.CommentPageSize })
|
||||
for index = 1, #rows do
|
||||
rows[index].verified = tonumber(rows[index].verified) == 1
|
||||
rows[index].is_owner = tonumber(rows[index].is_owner) == 1
|
||||
rows[index].is_liked = tonumber(rows[index].is_liked) == 1
|
||||
rows[index].like_count = tonumber(rows[index].like_count) or 0
|
||||
rows[index].created_at = (tonumber(rows[index].created_at_unix) or 0) * 1000
|
||||
rows[index].created_at_unix = nil
|
||||
end
|
||||
return { success = true, data = rows }
|
||||
end)
|
||||
|
||||
Bridge.Callbacks.Register("sky_phone:picstagram:comment-react", function(source, data)
|
||||
local profile, error_response = profile_for_session(source)
|
||||
if not profile then
|
||||
return error_response
|
||||
end
|
||||
if not SkyPhone.AllowOperation(source, "picstagram:comment-react", 60, 60) then
|
||||
return { success = false, error = "rate_limited" }
|
||||
end
|
||||
local comment_id = type(data) == "table" and data.id or nil
|
||||
if not valid_id(comment_id) or type(data.active) ~= "boolean" then
|
||||
return { success = false, error = "invalid_request" }
|
||||
end
|
||||
local comment = Bridge.Database.Query([[
|
||||
SELECT comment.`profile_id`, comment.`post_id`
|
||||
FROM `sky_phone_picstagram_comments` comment
|
||||
WHERE comment.`id` = ? AND comment.`status` = 'visible' LIMIT 1
|
||||
]], { comment_id })[1]
|
||||
if not comment or not find_accessible_post(comment.post_id, profile.id) then
|
||||
return { success = false, error = "invalid_comment" }
|
||||
end
|
||||
if are_profiles_blocked(profile.id, comment.profile_id) then
|
||||
return { success = false, error = "blocked" }
|
||||
end
|
||||
if data.active then
|
||||
local result = Bridge.Database.Query([[
|
||||
INSERT IGNORE INTO `sky_phone_picstagram_comment_reactions` (`comment_id`, `profile_id`)
|
||||
VALUES (?, ?)
|
||||
]], { comment_id, profile.id })
|
||||
if affected_rows(result) > 0 then
|
||||
create_activity(comment.profile_id, profile.id, "comment_like", comment.post_id)
|
||||
end
|
||||
else
|
||||
Bridge.Database.Query([[
|
||||
DELETE FROM `sky_phone_picstagram_comment_reactions`
|
||||
WHERE `comment_id` = ? AND `profile_id` = ?
|
||||
]], { comment_id, profile.id })
|
||||
end
|
||||
return { success = true }
|
||||
end)
|
||||
|
||||
Bridge.Callbacks.Register("sky_phone:picstagram:comment", function(source, data)
|
||||
if not SkyPhone.AllowOperation(source, "picstagram:comment", Config.Picstagram.CommentsPerMinute, 60) then
|
||||
return { success = false, error = "rate_limited" }
|
||||
@@ -905,16 +1001,32 @@ Bridge.Callbacks.Register("sky_phone:picstagram:comment", function(source, data)
|
||||
if tonumber(post.comments_enabled) ~= 1 then
|
||||
return { success = false, error = "comments_disabled" }
|
||||
end
|
||||
local parent_id = valid_id(data.parentId) and data.parentId or nil
|
||||
local parent
|
||||
if parent_id then
|
||||
parent = Bridge.Database.Query([[
|
||||
SELECT `id`, `profile_id` FROM `sky_phone_picstagram_comments`
|
||||
WHERE `id` = ? AND `post_id` = ? AND `parent_id` IS NULL AND `status` = 'visible' LIMIT 1
|
||||
]], { parent_id, data.id })[1]
|
||||
if not parent then
|
||||
if data.replyToId ~= nil and not valid_id(data.replyToId) then
|
||||
return { success = false, error = "invalid_comment" }
|
||||
end
|
||||
local parent_id
|
||||
local reply_target
|
||||
if data.replyToId then
|
||||
reply_target = Bridge.Database.Query([[
|
||||
SELECT `id`, `profile_id`, `parent_id` FROM `sky_phone_picstagram_comments`
|
||||
WHERE `id` = ? AND `post_id` = ? AND `status` = 'visible' LIMIT 1
|
||||
]], { data.replyToId, data.id })[1]
|
||||
if not reply_target or are_profiles_blocked(profile.id, reply_target.profile_id) then
|
||||
return { success = false, error = "invalid_comment" }
|
||||
end
|
||||
parent_id = reply_target.parent_id or reply_target.id
|
||||
elseif data.parentId then
|
||||
if not valid_id(data.parentId) then
|
||||
return { success = false, error = "invalid_comment" }
|
||||
end
|
||||
reply_target = Bridge.Database.Query([[
|
||||
SELECT `id`, `profile_id`, `parent_id` FROM `sky_phone_picstagram_comments`
|
||||
WHERE `id` = ? AND `post_id` = ? AND `parent_id` IS NULL AND `status` = 'visible' LIMIT 1
|
||||
]], { data.parentId, data.id })[1]
|
||||
if not reply_target or are_profiles_blocked(profile.id, reply_target.profile_id) then
|
||||
return { success = false, error = "invalid_comment" }
|
||||
end
|
||||
parent_id = reply_target.id
|
||||
end
|
||||
local id = new_id()
|
||||
if parent_id then
|
||||
@@ -928,9 +1040,11 @@ Bridge.Callbacks.Register("sky_phone:picstagram:comment", function(source, data)
|
||||
VALUES (?, ?, ?, ?)
|
||||
]], { id, data.id, profile.id, body })
|
||||
end
|
||||
create_activity(post.profile_id, profile.id, "comment", data.id)
|
||||
if parent and parent.profile_id ~= post.profile_id then
|
||||
create_activity(parent.profile_id, profile.id, "comment", data.id)
|
||||
if reply_target then
|
||||
create_activity(reply_target.profile_id, profile.id, "reply", data.id)
|
||||
end
|
||||
if not reply_target or reply_target.profile_id ~= post.profile_id then
|
||||
create_activity(post.profile_id, profile.id, "comment", data.id)
|
||||
end
|
||||
return { success = true, data = { id = id } }
|
||||
end)
|
||||
@@ -968,14 +1082,14 @@ Bridge.Callbacks.Register("sky_phone:picstagram:publish-story", function(source,
|
||||
if not profile then
|
||||
return error_response
|
||||
end
|
||||
if type(data) ~= "table" then
|
||||
if type(data) ~= "table" or (data.mediaType ~= "photo" and data.mediaType ~= "video") then
|
||||
return { success = false, error = "invalid_story" }
|
||||
end
|
||||
local media_id = tonumber(data.mediaId)
|
||||
local body = trim(data.body) or ""
|
||||
if not media_id or media_id < 1 or media_id ~= math.floor(media_id)
|
||||
or not valid_text(body, 0, Config.Picstagram.StoryTextMaxLength)
|
||||
or not SkyPhoneMedia.ResolveOwnedMedia(source, tostring(media_id), "photo")
|
||||
or not SkyPhoneMedia.ResolveOwnedMedia(source, tostring(media_id), data.mediaType)
|
||||
then
|
||||
return { success = false, error = "invalid_story" }
|
||||
end
|
||||
@@ -994,7 +1108,7 @@ Bridge.Callbacks.Register("sky_phone:picstagram:stories", function(source)
|
||||
return error_response
|
||||
end
|
||||
local rows = Bridge.Database.Query([[
|
||||
SELECT story.`id`, story.`profile_id`, story.`body`, media.`url`, author.`handle`,
|
||||
SELECT story.`id`, story.`profile_id`, story.`body`, media.`url`, media.`media_type`, author.`handle`,
|
||||
author.`display_name`, author.`verified`, avatar.`url` AS `avatar_url`,
|
||||
UNIX_TIMESTAMP(story.`created_at`) AS `created_at_unix`,
|
||||
UNIX_TIMESTAMP(story.`expires_at`) AS `expires_at_unix`,
|
||||
|
||||
@@ -725,6 +725,17 @@ CREATE TABLE IF NOT EXISTS `sky_phone_picstagram_comments` (
|
||||
FOREIGN KEY (`parent_id`) REFERENCES `sky_phone_picstagram_comments` (`id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `sky_phone_picstagram_comment_reactions` (
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`comment_id` CHAR(36) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
|
||||
`profile_id` CHAR(36) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
|
||||
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uniq_sky_phone_picstagram_comment_reaction` (`comment_id`,`profile_id`),
|
||||
FOREIGN KEY (`comment_id`) REFERENCES `sky_phone_picstagram_comments` (`id`) ON DELETE CASCADE,
|
||||
FOREIGN KEY (`profile_id`) REFERENCES `sky_phone_picstagram_profiles` (`id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `sky_phone_picstagram_stories` (
|
||||
`id` CHAR(36) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
|
||||
`profile_id` CHAR(36) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
|
||||
@@ -756,7 +767,7 @@ CREATE TABLE IF NOT EXISTS `sky_phone_picstagram_activities` (
|
||||
`recipient_id` CHAR(36) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
|
||||
`actor_id` CHAR(36) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
|
||||
`post_id` CHAR(36) CHARACTER SET ascii COLLATE ascii_bin NULL,
|
||||
`kind` ENUM('follow_request','follow','request_accepted','like','comment','verified') NOT NULL,
|
||||
`kind` ENUM('follow_request','follow','request_accepted','like','comment','comment_like','reply','verified') NOT NULL,
|
||||
`read_at` DATETIME NULL,
|
||||
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
|
||||
Reference in New Issue
Block a user