mirror of
https://github.com/sky-systems/sky_phone.git
synced 2026-08-30 17:59:02 +00:00
ENH - migrate DarkChat account flow to Sky UI
This commit is contained in:
@@ -2,7 +2,11 @@ import { createPinia, setActivePinia } from 'pinia'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { useDarkChatStore } from '@/stores/darkchat'
|
||||
import type { DarkChatMessage, DarkChatThread } from '@/types/darkchat'
|
||||
import type {
|
||||
DarkChatMessage,
|
||||
DarkChatProfile,
|
||||
DarkChatThread,
|
||||
} from '@/types/darkchat'
|
||||
import { nuiCall } from '@/utils/nui'
|
||||
|
||||
vi.mock('@/utils/nui', () => ({ nuiCall: vi.fn() }))
|
||||
@@ -18,6 +22,17 @@ const conversation: DarkChatThread['conversation'] = {
|
||||
readReceipts: true,
|
||||
}
|
||||
|
||||
const profile: DarkChatProfile = {
|
||||
activityVisible: false,
|
||||
alias: 'Nightshade',
|
||||
avatarSeed: 267,
|
||||
createdAt: '2026-08-01 21:20:00',
|
||||
darkId: 'dark:7X4K-P92D',
|
||||
id: 1,
|
||||
inviteCode: 'DC-7X4K-NOVA',
|
||||
notificationMode: 'private',
|
||||
}
|
||||
|
||||
function message(id: string): DarkChatMessage {
|
||||
return {
|
||||
body: 'Quiet channel',
|
||||
@@ -37,28 +52,53 @@ describe('darkchat store', () => {
|
||||
})
|
||||
|
||||
it('optimistically sends and confirms a private message', async () => {
|
||||
let resolveSend: ((value: { data: DarkChatMessage; success: true }) => void) | undefined
|
||||
const pending = new Promise<{ data: DarkChatMessage; success: true }>((resolve) => (resolveSend = resolve))
|
||||
let resolveSend:
|
||||
| ((value: { data: DarkChatMessage; success: true }) => void)
|
||||
| undefined
|
||||
const pending = new Promise<{ data: DarkChatMessage; success: true }>(
|
||||
(resolve) => (resolveSend = resolve),
|
||||
)
|
||||
mockNuiCall
|
||||
.mockResolvedValueOnce({ data: { conversation, messages: [] }, success: true })
|
||||
.mockResolvedValueOnce({ data: { contacts: [], conversations: [], profile: null }, success: true })
|
||||
.mockResolvedValueOnce({
|
||||
data: { conversation, messages: [] },
|
||||
success: true,
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
data: { contacts: [], conversations: [], profile: null },
|
||||
success: true,
|
||||
})
|
||||
.mockImplementationOnce(() => pending)
|
||||
.mockResolvedValueOnce({ data: { contacts: [], conversations: [], profile: null }, success: true })
|
||||
.mockResolvedValueOnce({
|
||||
data: { contacts: [], conversations: [], profile: null },
|
||||
success: true,
|
||||
})
|
||||
|
||||
const store = useDarkChatStore()
|
||||
await store.openThread(conversation.id)
|
||||
const sending = store.send({ body: 'Quiet channel', messageType: 'text' })
|
||||
expect(store.messages[0]).toMatchObject({ deliveryStatus: 'sending', body: 'Quiet channel' })
|
||||
expect(store.messages[0]).toMatchObject({
|
||||
deliveryStatus: 'sending',
|
||||
body: 'Quiet channel',
|
||||
})
|
||||
|
||||
resolveSend?.({ data: message('server-message'), success: true })
|
||||
await sending
|
||||
expect(store.messages[0]).toMatchObject({ deliveryStatus: 'delivered', id: 'server-message' })
|
||||
expect(store.messages[0]).toMatchObject({
|
||||
deliveryStatus: 'delivered',
|
||||
id: 'server-message',
|
||||
})
|
||||
})
|
||||
|
||||
it('keeps failed messages visible for delivery feedback', async () => {
|
||||
mockNuiCall
|
||||
.mockResolvedValueOnce({ data: { conversation, messages: [] }, success: true })
|
||||
.mockResolvedValueOnce({ data: { contacts: [], conversations: [], profile: null }, success: true })
|
||||
.mockResolvedValueOnce({
|
||||
data: { conversation, messages: [] },
|
||||
success: true,
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
data: { contacts: [], conversations: [], profile: null },
|
||||
success: true,
|
||||
})
|
||||
.mockResolvedValueOnce({ error: 'blocked', success: false })
|
||||
|
||||
const store = useDarkChatStore()
|
||||
@@ -69,10 +109,22 @@ describe('darkchat store', () => {
|
||||
|
||||
it('uses a local media preview without sending that URL to the server', async () => {
|
||||
mockNuiCall
|
||||
.mockResolvedValueOnce({ data: { conversation, messages: [] }, success: true })
|
||||
.mockResolvedValueOnce({ data: { contacts: [], conversations: [], profile: null }, success: true })
|
||||
.mockResolvedValueOnce({ data: { ...message('media-id'), messageType: 'image' }, success: true })
|
||||
.mockResolvedValueOnce({ data: { contacts: [], conversations: [], profile: null }, success: true })
|
||||
.mockResolvedValueOnce({
|
||||
data: { conversation, messages: [] },
|
||||
success: true,
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
data: { contacts: [], conversations: [], profile: null },
|
||||
success: true,
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
data: { ...message('media-id'), messageType: 'image' },
|
||||
success: true,
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
data: { contacts: [], conversations: [], profile: null },
|
||||
success: true,
|
||||
})
|
||||
|
||||
const store = useDarkChatStore()
|
||||
await store.openThread(conversation.id)
|
||||
@@ -102,7 +154,41 @@ describe('darkchat store', () => {
|
||||
const store = useDarkChatStore()
|
||||
expect(await store.loadMedia('voice-id')).toBe(true)
|
||||
expect(await store.loadMedia('voice-id')).toBe(true)
|
||||
expect(store.mediaSources['voice-id']).toBe('data:audio/webm;codecs=opus;base64,ZmFrZQ==')
|
||||
expect(store.mediaSources['voice-id']).toBe(
|
||||
'data:audio/webm;codecs=opus;base64,ZmFrZQ==',
|
||||
)
|
||||
expect(mockNuiCall).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('creates a private profile only through the explicit profile action', async () => {
|
||||
mockNuiCall.mockResolvedValueOnce({
|
||||
data: { contacts: [], conversations: [], profile },
|
||||
success: true,
|
||||
})
|
||||
|
||||
const store = useDarkChatStore()
|
||||
expect(await store.createProfile()).toBe(true)
|
||||
expect(store.profile).toEqual(profile)
|
||||
expect(mockNuiCall).toHaveBeenCalledWith('darkchat:create-profile')
|
||||
})
|
||||
|
||||
it('clears all private state after deleting the profile', async () => {
|
||||
mockNuiCall
|
||||
.mockResolvedValueOnce({
|
||||
data: { contacts: [], conversations: [], profile },
|
||||
success: true,
|
||||
})
|
||||
.mockResolvedValueOnce({ success: true })
|
||||
|
||||
const store = useDarkChatStore()
|
||||
await store.bootstrap()
|
||||
store.activeConversation = conversation
|
||||
store.messages = [message('message-id')]
|
||||
|
||||
expect(await store.deleteProfile()).toBe(true)
|
||||
expect(store.profile).toBeNull()
|
||||
expect(store.activeConversation).toBeNull()
|
||||
expect(store.messages).toEqual([])
|
||||
expect(mockNuiCall).toHaveBeenLastCalledWith('darkchat:delete-profile')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -23,7 +23,10 @@ export const useDarkChatStore = defineStore('darkchat', () => {
|
||||
const loading = ref(false)
|
||||
const lastError = ref<string | null>(null)
|
||||
const unreadCount = computed(() =>
|
||||
conversations.value.reduce((total, conversation) => total + conversation.unread, 0),
|
||||
conversations.value.reduce(
|
||||
(total, conversation) => total + conversation.unread,
|
||||
0,
|
||||
),
|
||||
)
|
||||
|
||||
async function bootstrap(): Promise<boolean> {
|
||||
@@ -37,15 +40,37 @@ export const useDarkChatStore = defineStore('darkchat', () => {
|
||||
conversations.value = []
|
||||
return false
|
||||
}
|
||||
profile.value = response.data.profile
|
||||
profile.value = response.data.profile ?? null
|
||||
contacts.value = response.data.contacts
|
||||
conversations.value = response.data.conversations
|
||||
return true
|
||||
}
|
||||
|
||||
async function createProfile(): Promise<boolean> {
|
||||
loading.value = true
|
||||
const response = await nuiCall<DarkChatBootstrap>('darkchat:create-profile')
|
||||
loading.value = false
|
||||
lastError.value = response.error ?? null
|
||||
if (!response.success || !response.data) return false
|
||||
profile.value = response.data.profile ?? null
|
||||
contacts.value = response.data.contacts
|
||||
conversations.value = response.data.conversations
|
||||
return true
|
||||
}
|
||||
|
||||
async function deleteProfile(): Promise<boolean> {
|
||||
const response = await nuiCall('darkchat:delete-profile')
|
||||
lastError.value = response.error ?? null
|
||||
if (!response.success) return false
|
||||
reset()
|
||||
return true
|
||||
}
|
||||
|
||||
async function openThread(conversationId: string): Promise<boolean> {
|
||||
loading.value = true
|
||||
const response = await nuiCall<DarkChatThread>('darkchat:thread', { conversationId })
|
||||
const response = await nuiCall<DarkChatThread>('darkchat:thread', {
|
||||
conversationId,
|
||||
})
|
||||
loading.value = false
|
||||
lastError.value = response.error ?? null
|
||||
if (!response.success || !response.data) return false
|
||||
@@ -61,20 +86,28 @@ export const useDarkChatStore = defineStore('darkchat', () => {
|
||||
async function refreshInbox(): Promise<boolean> {
|
||||
const response = await nuiCall<DarkChatBootstrap>('darkchat:bootstrap')
|
||||
if (!response.success || !response.data) return false
|
||||
profile.value = response.data.profile
|
||||
profile.value = response.data.profile ?? null
|
||||
contacts.value = response.data.contacts
|
||||
conversations.value = response.data.conversations
|
||||
return true
|
||||
}
|
||||
|
||||
async function start(identifier: string): Promise<NuiResponse<{ conversationId: string }>> {
|
||||
const response = await nuiCall<{ conversationId: string }>('darkchat:start', { identifier })
|
||||
async function start(
|
||||
identifier: string,
|
||||
): Promise<NuiResponse<{ conversationId: string }>> {
|
||||
const response = await nuiCall<{ conversationId: string }>(
|
||||
'darkchat:start',
|
||||
{ identifier },
|
||||
)
|
||||
if (response.success) await refreshInbox()
|
||||
return response
|
||||
}
|
||||
|
||||
async function send(outgoing: DarkChatOutgoing): Promise<NuiResponse<DarkChatMessage>> {
|
||||
if (!activeConversation.value) return { success: false, error: 'invalid_conversation' }
|
||||
async function send(
|
||||
outgoing: DarkChatOutgoing,
|
||||
): Promise<NuiResponse<DarkChatMessage>> {
|
||||
if (!activeConversation.value)
|
||||
return { success: false, error: 'invalid_conversation' }
|
||||
const clientId = `dark-pending-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
|
||||
const { mediaPreviewUrl, ...request } = outgoing
|
||||
const optimistic: DarkChatMessage = {
|
||||
@@ -100,13 +133,16 @@ export const useDarkChatStore = defineStore('darkchat', () => {
|
||||
}
|
||||
messages.value.push(optimistic)
|
||||
if (outgoing.messageType === 'voice' && outgoing.mediaPayload) {
|
||||
mediaSources.value[clientId] = `data:${outgoing.mediaMime};base64,${outgoing.mediaPayload}`
|
||||
mediaSources.value[clientId] =
|
||||
`data:${outgoing.mediaMime};base64,${outgoing.mediaPayload}`
|
||||
}
|
||||
const response = await nuiCall<DarkChatMessage>('darkchat:send', {
|
||||
...request,
|
||||
conversationId: activeConversation.value.id,
|
||||
})
|
||||
const index = messages.value.findIndex((message) => message.clientId === clientId)
|
||||
const index = messages.value.findIndex(
|
||||
(message) => message.clientId === clientId,
|
||||
)
|
||||
if (!response.success || !response.data) {
|
||||
if (index >= 0) messages.value[index].deliveryStatus = 'failed'
|
||||
return response
|
||||
@@ -115,7 +151,11 @@ export const useDarkChatStore = defineStore('darkchat', () => {
|
||||
delete mediaSources.value[clientId]
|
||||
if (source) mediaSources.value[response.data.id] = source
|
||||
if (index >= 0) {
|
||||
messages.value[index] = { ...response.data, clientId, deliveryStatus: 'delivered' }
|
||||
messages.value[index] = {
|
||||
...response.data,
|
||||
clientId,
|
||||
deliveryStatus: 'delivered',
|
||||
}
|
||||
}
|
||||
await refreshInbox()
|
||||
return response
|
||||
@@ -123,13 +163,20 @@ export const useDarkChatStore = defineStore('darkchat', () => {
|
||||
|
||||
async function loadMedia(messageId: string): Promise<boolean> {
|
||||
if (mediaSources.value[messageId]) return true
|
||||
const response = await nuiCall<{ mime: string; payload: string }>('darkchat:media', { messageId })
|
||||
const response = await nuiCall<{ mime: string; payload: string }>(
|
||||
'darkchat:media',
|
||||
{ messageId },
|
||||
)
|
||||
if (!response.success || !response.data) return false
|
||||
mediaSources.value[messageId] = `data:${response.data.mime};base64,${response.data.payload}`
|
||||
mediaSources.value[messageId] =
|
||||
`data:${response.data.mime};base64,${response.data.payload}`
|
||||
return true
|
||||
}
|
||||
|
||||
async function mutate(endpoint: string, data: Record<string, unknown>): Promise<boolean> {
|
||||
async function mutate(
|
||||
endpoint: string,
|
||||
data: Record<string, unknown>,
|
||||
): Promise<boolean> {
|
||||
const response = await nuiCall(`darkchat:${endpoint}`, data)
|
||||
return response.success
|
||||
}
|
||||
@@ -140,12 +187,25 @@ export const useDarkChatStore = defineStore('darkchat', () => {
|
||||
mediaSources.value = {}
|
||||
}
|
||||
|
||||
function reset(): void {
|
||||
profile.value = null
|
||||
contacts.value = []
|
||||
conversations.value = []
|
||||
activeConversation.value = null
|
||||
messages.value = []
|
||||
mediaSources.value = {}
|
||||
loading.value = false
|
||||
lastError.value = null
|
||||
}
|
||||
|
||||
return {
|
||||
activeConversation,
|
||||
bootstrap,
|
||||
closeThread,
|
||||
createProfile,
|
||||
contacts,
|
||||
conversations,
|
||||
deleteProfile,
|
||||
lastError,
|
||||
loadMedia,
|
||||
loading,
|
||||
@@ -155,6 +215,7 @@ export const useDarkChatStore = defineStore('darkchat', () => {
|
||||
openThread,
|
||||
profile,
|
||||
refreshInbox,
|
||||
reset,
|
||||
send,
|
||||
start,
|
||||
unreadCount,
|
||||
|
||||
@@ -1483,6 +1483,9 @@ const defaultLocales: LocaleTree = {
|
||||
signInBody:
|
||||
'DarkChat identities are linked to your private Sky Cloud account.',
|
||||
signInHint: 'Sign in through Settings to continue.',
|
||||
createIdentity: 'Create Dark Identity',
|
||||
createIdentityBody:
|
||||
'Create a private identity before starting a DarkChat conversation.',
|
||||
security: 'Security',
|
||||
privateNetwork: 'Private invitation-only network',
|
||||
noResults: 'No Results',
|
||||
@@ -1497,6 +1500,10 @@ const defaultLocales: LocaleTree = {
|
||||
darkIdOrInvite: 'Dark-ID or invitation code',
|
||||
continue: 'Continue',
|
||||
contacts: 'DarkChat Contacts',
|
||||
contactsHint:
|
||||
'Saved private identities appear only on your DarkChat account.',
|
||||
noContacts: 'No saved DarkChat contacts',
|
||||
private: 'Private',
|
||||
shareIdentity: 'Tap to share your private identity',
|
||||
message: 'Dark message',
|
||||
activeNow: 'Activity shared',
|
||||
@@ -1535,7 +1542,10 @@ const defaultLocales: LocaleTree = {
|
||||
deleteForMe: 'Delete for me',
|
||||
deleteForBoth: 'Delete for both',
|
||||
report: 'Report',
|
||||
messageActions: 'Message actions',
|
||||
contactSecurity: 'Contact & Security',
|
||||
chatSettings: 'Chat Settings',
|
||||
contactActions: 'Contact Actions',
|
||||
chatSince: 'Private chat since {date}',
|
||||
notifications: 'Notifications',
|
||||
readReceipts: 'Read receipts',
|
||||
@@ -1552,6 +1562,7 @@ const defaultLocales: LocaleTree = {
|
||||
chatCleared: 'Chat cleared',
|
||||
myIdentity: 'My Dark Identity',
|
||||
alias: 'Alias',
|
||||
privacySettings: 'Privacy Settings',
|
||||
notificationPrivacy: 'Notification privacy',
|
||||
notificationFull: 'Full · alias and message',
|
||||
notificationPrivate: 'Private · generic message',
|
||||
@@ -1561,6 +1572,19 @@ const defaultLocales: LocaleTree = {
|
||||
copyInvite: 'Copy Invite',
|
||||
privacyDisclaimer:
|
||||
'DarkChat stores messages on the server and does not claim end-to-end encryption.',
|
||||
profileActions: 'Account Actions',
|
||||
signOut: 'Sign Out',
|
||||
signOutTitle: 'Sign out of Sky Cloud?',
|
||||
signOutBody:
|
||||
'This signs the whole phone out of Sky Cloud. Your DarkChat profile and messages remain stored.',
|
||||
signingOut: 'Signing Out...',
|
||||
signOutHint:
|
||||
'Signing out affects every app that uses Sky Cloud on this phone.',
|
||||
deleteProfile: 'Delete DarkChat Profile',
|
||||
deleteProfileTitle: 'Delete DarkChat profile?',
|
||||
deleteProfileBody:
|
||||
'Your identity, contacts and DarkChat conversations will be permanently deleted.',
|
||||
deletingProfile: 'Deleting...',
|
||||
unknownIdentity: 'Unknown Identity',
|
||||
unknownIdentityBody:
|
||||
'Only continue if you expected this identity. The account is not discoverable through public search.',
|
||||
@@ -1588,6 +1612,7 @@ const defaultLocales: LocaleTree = {
|
||||
invalid_voice: 'This voice message is invalid.',
|
||||
invalid_attachment: 'This photo or video is unavailable.',
|
||||
invalid_profile: 'Check your alias and privacy settings.',
|
||||
sign_out_failed: 'Sky Cloud could not sign out.',
|
||||
rate_limited: 'Too many requests. Try again shortly.',
|
||||
gif_provider_unconfigured: 'GIF search is not configured.',
|
||||
gif_provider_unauthorized: 'The GIF provider key is invalid.',
|
||||
|
||||
@@ -79,7 +79,7 @@ export type DarkChatMessage = {
|
||||
}
|
||||
|
||||
export type DarkChatBootstrap = {
|
||||
profile: DarkChatProfile
|
||||
profile: DarkChatProfile | null
|
||||
contacts: DarkChatContact[]
|
||||
conversations: DarkChatConversationSummary[]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import { readFileSync } from 'node:fs'
|
||||
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
const source = readFileSync(
|
||||
new URL('./DarkChatApp.vue', import.meta.url),
|
||||
'utf8',
|
||||
)
|
||||
|
||||
describe('DarkChatApp Sky UI contract', () => {
|
||||
it('uses first-party Sky UI without direct Konsta markup', () => {
|
||||
expect(source).not.toContain("from 'konsta/vue'")
|
||||
expect(source).not.toMatch(/<\/?k-[a-z]/)
|
||||
expect(source).toContain('<SkyAppPage')
|
||||
expect(source).toContain('<SkyNavbar')
|
||||
expect(source).toContain('<SkyScrollArea')
|
||||
expect(source).toContain('<SkySettingsGroup')
|
||||
expect(source).toContain('<SkyMessagebar')
|
||||
expect(source).toContain('<SkyPillNavigation')
|
||||
})
|
||||
|
||||
it('keeps one identity action in the inbox header', () => {
|
||||
const inboxStart = source.indexOf("screen === 'inbox'")
|
||||
const newChatStart = source.indexOf("screen === 'new'")
|
||||
const inbox = source.slice(inboxStart, newChatStart)
|
||||
|
||||
expect(inbox.match(/@click="openProfile"/g)).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('bottom-aligns short threads and exposes profile lifecycle actions', () => {
|
||||
expect(source).toContain('ref="messagesArea"')
|
||||
expect(source).toMatch(/\.dc-day\s*\{[^}]*margin:\s*auto 0 8px/s)
|
||||
expect(source).toContain('@click="signOut"')
|
||||
expect(source).toContain('@click="deleteProfile"')
|
||||
})
|
||||
|
||||
it('separates the round attachment action from the floating message pill', () => {
|
||||
expect(source).toContain('class="dc-composer-row"')
|
||||
expect(source).toContain('class="dc-composer-action"')
|
||||
expect(source).toContain('class="dc-composer-pill"')
|
||||
expect(source).toMatch(
|
||||
/<div v-else class="dc-composer-row">[\s\S]*?<SkyGlass[\s\S]*?class="dc-composer-action"[\s\S]*?<SkyGlass class="dc-composer-pill">[\s\S]*?<SkyMessagebar/,
|
||||
)
|
||||
expect(source).not.toContain('<template #left>')
|
||||
expect(source).toMatch(
|
||||
/\.dc-composer-pill\s*\{[^}]*border-radius:\s*var\(--sky-radius-pill\)/s,
|
||||
)
|
||||
expect(source).toMatch(
|
||||
/\.dc-composer-action\s*\{[^}]*border-radius:\s*50%/s,
|
||||
)
|
||||
})
|
||||
|
||||
it('raises only the DarkChat inbox title block', () => {
|
||||
expect(source).toContain('class="dc-inbox-navbar"')
|
||||
expect(source).toMatch(
|
||||
/\.dc-inbox-navbar :deep\(\.sky-navbar__title-container > div\)\s*\{[^}]*translateY\(-6px\)/s,
|
||||
)
|
||||
})
|
||||
})
|
||||
+1030
-1372
File diff suppressed because it is too large
Load Diff
@@ -1666,6 +1666,7 @@ const darkChatProfile = {
|
||||
activityVisible: false,
|
||||
createdAt: '2026-08-01 21:20:00',
|
||||
}
|
||||
let darkChatProfileActive = true
|
||||
const darkChatPeers = [
|
||||
{
|
||||
id: 2,
|
||||
@@ -1758,6 +1759,9 @@ const darkChatMessages = [
|
||||
]
|
||||
|
||||
function darkChatBootstrap() {
|
||||
if (!darkChatProfileActive) {
|
||||
return { profile: null, contacts: [], conversations: [] }
|
||||
}
|
||||
return {
|
||||
profile: darkChatProfile,
|
||||
contacts: darkChatPeers
|
||||
@@ -7304,6 +7308,16 @@ app.post('/api/:endpoint', (request, response) => {
|
||||
response.json({ success: true, data: darkChatBootstrap() })
|
||||
return
|
||||
}
|
||||
if (endpoint === 'darkchat:create-profile') {
|
||||
darkChatProfileActive = true
|
||||
response.json({ success: true, data: darkChatBootstrap() })
|
||||
return
|
||||
}
|
||||
if (endpoint === 'darkchat:delete-profile') {
|
||||
darkChatProfileActive = false
|
||||
response.json({ success: true })
|
||||
return
|
||||
}
|
||||
if (endpoint === 'darkchat:update-profile') {
|
||||
darkChatProfile.alias = String(
|
||||
request.body.alias ?? darkChatProfile.alias,
|
||||
|
||||
@@ -482,9 +482,10 @@ Locales["en"] = {
|
||||
darkchat = {
|
||||
name = "DarkChat", newMessage = "New DarkChat message from {sender}", privateNotification = "New DarkChat message",
|
||||
signInBody = "DarkChat identities are linked to your private Sky Cloud account.", signInHint = "Sign in through Settings to continue.",
|
||||
createIdentity = "Create Dark Identity", createIdentityBody = "Create a private identity before starting a DarkChat conversation.",
|
||||
security = "Security", privateNetwork = "Private invitation-only network", noResults = "No Results", noResultsBody = "Try another alias or Dark-ID.",
|
||||
noChats = "No Private Chats", noChatsBody = "Connect with a Dark-ID or invitation code. There is no public search.", newChat = "New Chat",
|
||||
connectPrivately = "Connect privately", newChatBody = "Enter an exact Dark-ID or invitation code. Unknown identities require confirmation.", darkIdOrInvite = "Dark-ID or invitation code", continue = "Continue", contacts = "DarkChat Contacts", shareIdentity = "Tap to share your private identity",
|
||||
connectPrivately = "Connect privately", newChatBody = "Enter an exact Dark-ID or invitation code. Unknown identities require confirmation.", darkIdOrInvite = "Dark-ID or invitation code", continue = "Continue", contacts = "DarkChat Contacts", contactsHint = "Saved private identities appear only on your DarkChat account.", noContacts = "No saved DarkChat contacts", private = "Private", shareIdentity = "Tap to share your private identity",
|
||||
message = "Dark message", activeNow = "Activity shared", encryptedSession = "Private session", serverPrivate = "Private server-stored conversation",
|
||||
emoji = "Emoji", gif = "GIF", gifs = "GIFs", photo = "Photo", video = "Video", attachPhoto = "Attach Photo", takePhoto = "Take Photo", attachGif = "Attach GIF", attachVideo = "Attach Video", searchGifs = "Search GIFs", loadMore = "Load More",
|
||||
voiceMessage = "Voice message", sending = "Sending", failed = "Not delivered", delivered = "Delivered", read = "Read", replying = "Replying to",
|
||||
@@ -493,10 +494,13 @@ Locales["en"] = {
|
||||
copied = "Copied", reply = "Reply", copy = "Copy", deleteForMe = "Delete for me", deleteForBoth = "Delete for both", report = "Report",
|
||||
contactSecurity = "Contact & Security", chatSince = "Private chat since {date}", notifications = "Notifications", readReceipts = "Read receipts", disappearing = "Disappearing messages", contactAlias = "Contact alias", saveContact = "Save Contact", addContact = "Add Contact", contactSaved = "Contact saved", removeContact = "Remove Contact", block = "Block User", unblock = "Unblock User", clearChat = "Clear Chat", chatCleared = "Chat cleared",
|
||||
myIdentity = "My Dark Identity", alias = "Alias", notificationPrivacy = "Notification privacy", notificationFull = "Full · alias and message", notificationPrivate = "Private · generic message", notificationHidden = "Invisible · badge only", shareActivity = "Share activity status", inviteCode = "Private invitation code", copyInvite = "Copy Invite", privacyDisclaimer = "DarkChat stores messages on the server and does not claim end-to-end encryption.",
|
||||
messageActions = "Message actions", chatSettings = "Chat Settings", contactActions = "Contact Actions", privacySettings = "Privacy Settings",
|
||||
profileActions = "Account Actions", signOut = "Sign Out", signOutTitle = "Sign out of Sky Cloud?", signOutBody = "This signs the whole phone out of Sky Cloud. Your DarkChat profile and messages remain stored.", signingOut = "Signing Out...", signOutHint = "Signing out affects every app that uses Sky Cloud on this phone.", deleteProfile = "Delete DarkChat Profile", deleteProfileTitle = "Delete DarkChat profile?", deleteProfileBody = "Your identity, contacts and DarkChat conversations will be permanently deleted.", deletingProfile = "Deleting...",
|
||||
unknownIdentity = "Unknown Identity", unknownIdentityBody = "Only continue if you expected this identity. The account is not discoverable through public search.", openSecureChat = "Open Private Chat",
|
||||
reportUser = "Report User", reportSpam = "Spam", reportHarassment = "Harassment", reportThreats = "Threats", reportIllegal = "Illegal content", reportOther = "Other", reportDetails = "Optional details", submitReport = "Submit Report", reported = "Report submitted",
|
||||
microphoneUnavailable = "The microphone is unavailable.", recordingTooLarge = "The voice message is too large.",
|
||||
errors = {
|
||||
sign_out_failed = "Sky Cloud could not sign out.",
|
||||
not_authenticated = "Sign in to your Sky Cloud account first.", invalid_dark_id = "Enter a valid Dark-ID or invitation code.", profile_not_found = "This private identity was not found.", self_chat = "You cannot message your own identity.", conversation_not_found = "This conversation is unavailable.", blocked = "Messages are blocked in this conversation.", invalid_message = "Enter a valid message.", invalid_gif = "This GIF is invalid.", invalid_voice = "This voice message is invalid.", invalid_attachment = "This photo or video is unavailable.", invalid_profile = "Check your alias and privacy settings.", rate_limited = "Too many requests. Try again shortly.", gif_provider_unconfigured = "GIF search is not configured.", gif_provider_unauthorized = "The GIF provider key is invalid.", gif_provider_rate_limited = "GIF search is busy. Try again shortly.", gif_provider_failed = "GIFs are temporarily unavailable.", default = "DarkChat could not complete the request.",
|
||||
},
|
||||
},
|
||||
|
||||
@@ -246,6 +246,8 @@ local server_callbacks = {
|
||||
"easyshare:respond",
|
||||
"easyshare:cancel",
|
||||
"darkchat:bootstrap",
|
||||
"darkchat:create-profile",
|
||||
"darkchat:delete-profile",
|
||||
"darkchat:update-profile",
|
||||
"darkchat:start",
|
||||
"darkchat:thread",
|
||||
|
||||
@@ -117,7 +117,10 @@ local function require_profile(source)
|
||||
if not account then
|
||||
return nil, nil, error_response
|
||||
end
|
||||
local profile = load_profile(account.id) or create_profile(account.id)
|
||||
local profile = load_profile(account.id)
|
||||
if not profile then
|
||||
return nil, account, { success = false, error = "profile_not_found" }
|
||||
end
|
||||
return profile, account
|
||||
end
|
||||
|
||||
@@ -340,10 +343,21 @@ local function valid_voice(data)
|
||||
end
|
||||
|
||||
Bridge.Callbacks.Register("sky_phone:darkchat:bootstrap", function(source)
|
||||
local profile, _, error_response = require_profile(source)
|
||||
if not profile then
|
||||
local account, error_response = SkyPhone.RequireAccount(source)
|
||||
if not account then
|
||||
return error_response
|
||||
end
|
||||
local profile = load_profile(account.id)
|
||||
if not profile then
|
||||
return {
|
||||
success = true,
|
||||
data = {
|
||||
profile = nil,
|
||||
contacts = {},
|
||||
conversations = {},
|
||||
},
|
||||
}
|
||||
end
|
||||
return {
|
||||
success = true,
|
||||
data = {
|
||||
@@ -354,6 +368,52 @@ Bridge.Callbacks.Register("sky_phone:darkchat:bootstrap", function(source)
|
||||
}
|
||||
end)
|
||||
|
||||
Bridge.Callbacks.Register("sky_phone:darkchat:create-profile", function(source)
|
||||
if not SkyPhone.AllowOperation(source, "darkchat_profile_create", 5, 60) then
|
||||
return { success = false, error = "rate_limited" }
|
||||
end
|
||||
local account, error_response = SkyPhone.RequireAccount(source)
|
||||
if not account then
|
||||
return error_response
|
||||
end
|
||||
local profile = load_profile(account.id) or create_profile(account.id)
|
||||
return {
|
||||
success = true,
|
||||
data = {
|
||||
profile = profile_payload(profile),
|
||||
contacts = list_contacts(profile.id),
|
||||
conversations = list_conversations(profile.id),
|
||||
},
|
||||
}
|
||||
end)
|
||||
|
||||
Bridge.Callbacks.Register("sky_phone:darkchat:delete-profile", function(source)
|
||||
if not SkyPhone.AllowOperation(source, "darkchat_profile_delete", 3, 60) then
|
||||
return { success = false, error = "rate_limited" }
|
||||
end
|
||||
local profile, account, error_response = require_profile(source)
|
||||
if not profile then
|
||||
return error_response
|
||||
end
|
||||
if not Bridge.Database.Transaction({
|
||||
{
|
||||
query = [[
|
||||
DELETE conversation FROM `sky_phone_darkchat_conversations` conversation
|
||||
JOIN `sky_phone_darkchat_members` member ON member.`conversation_id` = conversation.`id`
|
||||
WHERE member.`profile_id` = ?
|
||||
]],
|
||||
params = { profile.id },
|
||||
},
|
||||
{
|
||||
query = "DELETE FROM `sky_phone_darkchat_profiles` WHERE `id` = ? AND `account_id` = ?",
|
||||
params = { profile.id, account.id },
|
||||
},
|
||||
}) then
|
||||
error("[sky_phone] Failed to delete a DarkChat profile transaction.")
|
||||
end
|
||||
return { success = true }
|
||||
end)
|
||||
|
||||
Bridge.Callbacks.Register("sky_phone:darkchat:update-profile", function(source, data)
|
||||
if not SkyPhone.AllowOperation(source, "darkchat_profile", 10, 60) then
|
||||
return { success = false, error = "rate_limited" }
|
||||
|
||||
Reference in New Issue
Block a user