mirror of
https://github.com/mudita/mudita-center.git
synced 2025-12-23 22:28:03 -05:00
Merge branch 'master' into fix-auto-commit
This commit is contained in:
3
.github/ISSUE_TEMPLATE/4-i18n.md
vendored
3
.github/ISSUE_TEMPLATE/4-i18n.md
vendored
@@ -1,11 +1,10 @@
|
||||
---
|
||||
name: "🏳 Create a new localization"
|
||||
about: Create a new localization in Mudita Center
|
||||
|
||||
---
|
||||
|
||||
# 🏳 Create a new localization
|
||||
|
||||
## 📝 Describe the solution you'd like
|
||||
|
||||
A clear and concise description of what localization you would like to create or improve.
|
||||
A clear and concise description of what localization you would like to create or improve.
|
||||
|
||||
@@ -37,4 +37,4 @@ Please refer to the [Development workflow article](development_workflow.md) to l
|
||||
|
||||
If you want to start localizing Mudita Center interface please start from [the "Internationalization" article](./i18n.md).
|
||||
|
||||
**Note:** Mudita Center comes with both [lint-staged](https://github.com/okonet/lint-staged) and [Prettier](https://prettier.io/) setup to ensure a consistent code style.
|
||||
**Note:** Mudita Center comes with both [lint-staged](https://github.com/okonet/lint-staged) and [Prettier](https://prettier.io/) setup to ensure a consistent code style.
|
||||
|
||||
3
i18n.md
3
i18n.md
@@ -3,6 +3,7 @@
|
||||
## File and data structure
|
||||
|
||||
### Display language
|
||||
|
||||
Currently all the wording used in Mudita Center interface is collected in the form of JSON files which are located in [the src/renderer/locales/default folder](./src/renderer/locales/default/).
|
||||
|
||||
The default fallback file for every language is currently the English version - `en-US.json`.
|
||||
@@ -14,4 +15,4 @@ Mudita Center core development team uses [Phrase](https://phrase.com/) software
|
||||
1. Create an issue with the localization you want to start working on. Please use the following scheme for the title of the issue: `[Language] localization [emoji_flag]` eg. `Polish localization 🇵🇱`. The emoji flag is a small detail that can help other community members in finding the localization they're interested in and helping you out in implementing it. Please make sure that the localization you want to implement has not been already implemented.
|
||||
2. Add a `i18n` label to your new issue on GitHub.
|
||||
3. Follow [the "Contributing to Mudita Center" article](./CONTRIBUTING.md).
|
||||
4. As soon as you create a Pull Request with your localization we will review it and add it to the official Mudita Center distribution.
|
||||
4. As soon as you create a Pull Request with your localization we will review it and add it to the official Mudita Center distribution.
|
||||
|
||||
@@ -69,11 +69,13 @@ export const ErrorDataModal = ({
|
||||
)
|
||||
|
||||
export const ErrorWithRetryDataModal = ({
|
||||
onClose = noop,
|
||||
onRetry,
|
||||
title = intl.formatMessage(messages.errorTitle),
|
||||
textMessage = messages.errorText,
|
||||
descriptionMessage = messages.errorDescription,
|
||||
}: {
|
||||
onClose?: () => void
|
||||
onRetry: () => void
|
||||
title?: string
|
||||
textMessage?: Message
|
||||
@@ -83,6 +85,7 @@ export const ErrorWithRetryDataModal = ({
|
||||
title={title}
|
||||
onActionButtonClick={onRetry}
|
||||
actionButtonLabel={intl.formatMessage(messages.errorWithRetryButton)}
|
||||
onClose={onClose}
|
||||
>
|
||||
<RoundIconWrapper>
|
||||
<Icon type={Type.Fail} width={4} />
|
||||
|
||||
@@ -362,14 +362,16 @@ const ContactList: FunctionComponent<ContactListProps> = ({
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
{resultsState === ResultsState.Empty && (
|
||||
<EmptyState
|
||||
title={{ id: "view.name.phone.contacts.emptyList.title" }}
|
||||
description={{
|
||||
id: "view.name.phone.contacts.emptyList.emptyPhonebook.description",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{resultsState === ResultsState.Empty ||
|
||||
(resultsState === ResultsState.Error && (
|
||||
<EmptyState
|
||||
title={{ id: "view.name.phone.contacts.emptyList.title" }}
|
||||
description={{
|
||||
id:
|
||||
"view.name.phone.contacts.emptyList.emptyPhonebook.description",
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
{resultsState === ResultsState.Loading && <LoadingState />}
|
||||
</SelectableContacts>
|
||||
)
|
||||
|
||||
@@ -42,7 +42,7 @@ export interface ContactPanelProps {
|
||||
selectedContacts: Contact[]
|
||||
allItemsSelected?: boolean
|
||||
toggleAll?: UseTableSelect<Contact>["toggleAll"]
|
||||
deleteContacts: (id: ContactID[]) => Promise<string | void>
|
||||
deleteContacts: (ids: ContactID[]) => Promise<string | void>
|
||||
resetRows: UseTableSelect<Contact>["resetRows"]
|
||||
manageButtonDisabled?: boolean
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
ContactFactorySignature,
|
||||
ContactID,
|
||||
Phone,
|
||||
PhoneState,
|
||||
} from "Renderer/models/phone/phone.typings"
|
||||
import { deburr, find, filter, omit } from "lodash"
|
||||
import { intl } from "Renderer/utils/intl"
|
||||
@@ -102,11 +103,11 @@ export const contactDatabaseFactory = (
|
||||
}
|
||||
|
||||
export const addContacts = (
|
||||
state: Phone,
|
||||
state: PhoneState,
|
||||
input: Contact | Contact[],
|
||||
factory: (input: Contact[]) => Phone = contactDatabaseFactory,
|
||||
preFormatter = prepareData
|
||||
) => {
|
||||
): PhoneState => {
|
||||
const result = factory(preFormatter(input))
|
||||
|
||||
if (result) {
|
||||
@@ -125,10 +126,10 @@ export const addContacts = (
|
||||
}
|
||||
|
||||
export const removeContact = (
|
||||
state: Phone,
|
||||
state: PhoneState,
|
||||
input: ContactID | ContactID[],
|
||||
preFormatter = prepareData
|
||||
) => {
|
||||
): Phone => {
|
||||
const inputArray = Array.isArray(input) ? input : [input]
|
||||
const { collection: oldCollection, db: oldDb } = state
|
||||
const data = preFormatter(input)
|
||||
@@ -145,8 +146,8 @@ export const removeContact = (
|
||||
return { db, collection }
|
||||
}
|
||||
|
||||
export const updateContact = (
|
||||
state: Phone,
|
||||
export const editContact = (
|
||||
state: PhoneState,
|
||||
data: BaseContactModel,
|
||||
guard: (input: any) => boolean = contactTypeGuard
|
||||
): Phone => {
|
||||
@@ -285,10 +286,10 @@ export const findMultipleContacts = (
|
||||
}
|
||||
|
||||
export const revokeField = (
|
||||
state: Phone,
|
||||
state: PhoneState,
|
||||
query: SimpleRecord,
|
||||
finder = findContact
|
||||
): Phone => {
|
||||
): PhoneState => {
|
||||
const userId = finder(state, query, true)
|
||||
|
||||
if (userId && typeof userId === "string") {
|
||||
@@ -300,8 +301,8 @@ export const revokeField = (
|
||||
db: {
|
||||
...state.db,
|
||||
[userId]: userData,
|
||||
},
|
||||
} as Phone
|
||||
} as Record<ContactID, Contact>,
|
||||
}
|
||||
}
|
||||
|
||||
return state
|
||||
|
||||
@@ -9,10 +9,15 @@ import {
|
||||
contactDatabaseFactory,
|
||||
addContacts,
|
||||
removeContact,
|
||||
updateContact,
|
||||
editContact,
|
||||
findContact,
|
||||
} from "Renderer/models/phone/phone.helpers"
|
||||
import { Contact, ContactID } from "Renderer/models/phone/phone.typings"
|
||||
import {
|
||||
Contact,
|
||||
ContactID,
|
||||
PhoneState,
|
||||
ResultsState,
|
||||
} from "Renderer/models/phone/phone.typings"
|
||||
import phone from "Renderer/models/phone/phone"
|
||||
|
||||
const TEST_CONTACT = { ...phoneSeed.db[phoneSeed.collection[0]] }
|
||||
@@ -36,11 +41,12 @@ const TEST_CONTACT_TO_CLEAN = {
|
||||
}
|
||||
const TEST_PHONE_NUMBER = "+82 707 439 683"
|
||||
const TEST_EXPECTED_PHONE_NUMBER = "+82707439683"
|
||||
const OLD_DB_SHAPE = {
|
||||
const OLD_DB_SHAPE: PhoneState = {
|
||||
db: {
|
||||
[TEST_CONTACT_TO_CLEAN.id]: TEST_CONTACT_TO_CLEAN,
|
||||
},
|
||||
collection: [TEST_CONTACT_TO_CLEAN.id],
|
||||
resultsState: ResultsState.Empty,
|
||||
}
|
||||
|
||||
describe("typeGuard tests", () => {
|
||||
@@ -131,7 +137,14 @@ describe("contactDatabaseFactory and mergeContacts tests", () => {
|
||||
})
|
||||
|
||||
test("should add contacts in batch", () => {
|
||||
const result = addContacts({ db: {}, collection: [] }, TEST_CONTACTS_BATCH)
|
||||
const result = addContacts(
|
||||
{
|
||||
db: {},
|
||||
collection: [],
|
||||
resultsState: ResultsState.Empty,
|
||||
},
|
||||
TEST_CONTACTS_BATCH
|
||||
)
|
||||
|
||||
expect(result.collection).toHaveLength(TEST_CONTACTS_BATCH.length)
|
||||
})
|
||||
@@ -149,7 +162,7 @@ describe("contactDatabaseFactory and mergeContacts tests", () => {
|
||||
|
||||
expect(OLD_DB_SHAPE.db[ID_TO_EDIT].firstName).not.toBe(NEW_NAME)
|
||||
|
||||
const result = updateContact(OLD_DB_SHAPE, {
|
||||
const result = editContact(OLD_DB_SHAPE, {
|
||||
id: ID_TO_EDIT,
|
||||
firstName: NEW_NAME,
|
||||
})
|
||||
@@ -213,7 +226,7 @@ describe("redux tests", () => {
|
||||
modifiedContact
|
||||
)
|
||||
|
||||
store.dispatch.phone.updateContact(modifiedContact)
|
||||
store.dispatch.phone.editContact(modifiedContact)
|
||||
|
||||
expect(store.getState().phone.db[TEST_CONTACT.id]).toMatchObject(
|
||||
modifiedContact
|
||||
@@ -256,7 +269,7 @@ describe("redux tests", () => {
|
||||
store.getState().phone.db[contactWithSpeedDial as ContactID].speedDial
|
||||
).toBe(speedDial)
|
||||
|
||||
store.dispatch.phone.updateContact({
|
||||
store.dispatch.phone.editContact({
|
||||
...contactToEdit,
|
||||
speedDial,
|
||||
})
|
||||
|
||||
@@ -3,15 +3,16 @@ import { Dispatch } from "Renderer/store"
|
||||
import {
|
||||
BaseContactModel,
|
||||
Contact,
|
||||
NewContact,
|
||||
ContactID,
|
||||
Phone,
|
||||
PhoneState,
|
||||
ResultsState,
|
||||
StoreData,
|
||||
} from "Renderer/models/phone/phone.typings"
|
||||
import {
|
||||
addContacts,
|
||||
contactDatabaseFactory,
|
||||
updateContact,
|
||||
editContact,
|
||||
getFlatList,
|
||||
getSortedContactList,
|
||||
getSpeedDialChosenList,
|
||||
@@ -19,17 +20,15 @@ import {
|
||||
revokeField,
|
||||
} from "Renderer/models/phone/phone.helpers"
|
||||
import { isContactMatchingPhoneNumber } from "Renderer/models/phone/is-contact-matching-phone-number"
|
||||
import getContacts from "Renderer/requests/get-contacts.request"
|
||||
import logger from "App/main/utils/logger"
|
||||
import externalProvidersStore from "Renderer/store/external-providers"
|
||||
import { Provider } from "Renderer/models/external-providers/external-providers.interface"
|
||||
import getContactsRequest from "Renderer/requests/get-contacts.request"
|
||||
import addContactRequest from "Renderer/requests/add-contact.request"
|
||||
import editContactRequest from "Renderer/requests/edit-contact.request"
|
||||
import deleteContactsRequest from "Renderer/requests/delete-contacts.request"
|
||||
import logger from "App/main/utils/logger"
|
||||
|
||||
export const initialState: Phone = {
|
||||
export const initialState: PhoneState = {
|
||||
db: {},
|
||||
collection: [],
|
||||
resultsState: ResultsState.Empty,
|
||||
}
|
||||
|
||||
let writeTrials = 0
|
||||
@@ -67,10 +66,13 @@ const simulateWriteToPhone = async (time = 2000) => {
|
||||
export default {
|
||||
state: initialState,
|
||||
reducers: {
|
||||
setContacts(state: Phone, contacts: Contact[]): Phone {
|
||||
return contactDatabaseFactory(contacts)
|
||||
setResultsState(state: PhoneState, resultsState: ResultsState): PhoneState {
|
||||
return { ...state, resultsState }
|
||||
},
|
||||
addContact(state: Phone, contact: Contact): Phone {
|
||||
setContacts(state: PhoneState, phone: Phone): PhoneState {
|
||||
return { ...state, ...phone }
|
||||
},
|
||||
addContact(state: PhoneState, contact: Contact): PhoneState {
|
||||
let currentState = state
|
||||
|
||||
/**
|
||||
@@ -81,22 +83,26 @@ export default {
|
||||
currentState = revokeField(state, { speedDial: contact.speedDial })
|
||||
}
|
||||
|
||||
return addContacts(currentState, contact)
|
||||
return { ...state, ...addContacts(currentState, contact) }
|
||||
},
|
||||
updateContact(state: Phone, data: BaseContactModel): Phone {
|
||||
editContact(state: PhoneState, data: BaseContactModel): PhoneState {
|
||||
let currentState = state
|
||||
|
||||
if (data.speedDial) {
|
||||
currentState = revokeField(state, { speedDial: data.speedDial })
|
||||
}
|
||||
|
||||
return updateContact(currentState, data)
|
||||
return { ...state, ...editContact(currentState, data) }
|
||||
},
|
||||
removeContact(state: Phone, input: ContactID | ContactID[]): Phone {
|
||||
return removeContact(state, input)
|
||||
removeContact(
|
||||
state: PhoneState,
|
||||
input: ContactID | ContactID[]
|
||||
): PhoneState {
|
||||
return { ...state, ...removeContact(state, input) }
|
||||
},
|
||||
updateContacts(state: Phone, contacts: Phone) {
|
||||
updateContacts(state: PhoneState, contacts: Phone) {
|
||||
return {
|
||||
...state,
|
||||
db: { ...state.db, ...contacts.db },
|
||||
collection: [...state.collection, ...contacts.collection],
|
||||
}
|
||||
@@ -107,13 +113,23 @@ export default {
|
||||
* about phone sync flow at the moment.
|
||||
*/
|
||||
effects: (dispatch: Dispatch) => ({
|
||||
loadData: async (): Promise<string | void> => {
|
||||
const { data = [], error } = await getContactsRequest()
|
||||
async loadData(
|
||||
_: any,
|
||||
rootState: { phone: { resultsState: ResultsState } }
|
||||
) {
|
||||
if (rootState.phone.resultsState === ResultsState.Loading) {
|
||||
return
|
||||
}
|
||||
|
||||
dispatch.phone.setResultsState(ResultsState.Loading)
|
||||
|
||||
const { data = [], error } = await getContacts()
|
||||
if (error) {
|
||||
logger.error(error)
|
||||
return error.message
|
||||
dispatch.phone.setResultsState(ResultsState.Error)
|
||||
} else {
|
||||
dispatch.phone.setContacts(data)
|
||||
dispatch.phone.setContacts(contactDatabaseFactory(data))
|
||||
dispatch.phone.setResultsState(ResultsState.Loaded)
|
||||
}
|
||||
},
|
||||
async loadContacts(provider: Provider) {
|
||||
@@ -125,35 +141,6 @@ export default {
|
||||
dispatch.phone.updateContacts(contactDatabaseFactory(contacts))
|
||||
}
|
||||
},
|
||||
addNewContact: async (contact: NewContact): Promise<string | void> => {
|
||||
const { data, error } = await addContactRequest(contact)
|
||||
if (error || !data) {
|
||||
logger.error(error)
|
||||
return error?.message ?? "Something went wrong"
|
||||
} else {
|
||||
dispatch.phone.addContact(data)
|
||||
}
|
||||
},
|
||||
editContact: async (contact: Contact): Promise<string | void> => {
|
||||
const { data, error } = await editContactRequest(contact)
|
||||
if (error || !data) {
|
||||
logger.error(error)
|
||||
return error?.message ?? "Something went wrong"
|
||||
} else {
|
||||
dispatch.phone.updateContact(data)
|
||||
}
|
||||
},
|
||||
async deleteContacts(input: ContactID[]): Promise<string | void> {
|
||||
const { error } = await deleteContactsRequest(input)
|
||||
if (error) {
|
||||
logger.error(error)
|
||||
const successIds = input.filter((id) => !error.data?.includes(id))
|
||||
dispatch.phone.removeContact(successIds)
|
||||
return error.message
|
||||
} else {
|
||||
dispatch.phone.removeContact(input)
|
||||
}
|
||||
},
|
||||
}),
|
||||
selectors: (slice: Slicer<StoreData>) => ({
|
||||
contactList() {
|
||||
|
||||
@@ -70,25 +70,25 @@ export enum ResultsState {
|
||||
Loading,
|
||||
Loaded,
|
||||
Empty,
|
||||
Error,
|
||||
}
|
||||
|
||||
export interface StoreData {
|
||||
inputValue: string
|
||||
contacts: Contact[]
|
||||
savingContact: boolean
|
||||
resultsState?: ResultsState
|
||||
resultsState: ResultsState
|
||||
}
|
||||
|
||||
export type PhoneState = Phone & Pick<StoreData, "resultsState">
|
||||
|
||||
interface StoreSelectors extends Contacts {
|
||||
speedDialContacts: Contact[]
|
||||
savingContact: boolean
|
||||
}
|
||||
|
||||
interface StoreEffects {
|
||||
readonly loadData: () => Promise<string | void>
|
||||
readonly addNewContact: (contact: NewContact) => Promise<string | void>
|
||||
readonly editContact: (data: Contact) => Promise<string | void>
|
||||
readonly deleteContacts?: (contacts: Contact[]) => void
|
||||
readonly loadData: () => Promise<void>
|
||||
readonly loadContacts: (provider: Provider) => Promise<Phone>
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useState } from "react"
|
||||
import React, { useEffect, useRef, useState } from "react"
|
||||
import Button from "Renderer/components/core/button/button.component"
|
||||
import ContactList from "Renderer/components/rest/phone/contact-list.component"
|
||||
import ContactPanel, {
|
||||
@@ -77,6 +77,9 @@ export type PhoneProps = ContactActions &
|
||||
onManageButtonClick: (cb?: any) => Promise<void>
|
||||
isTopicThreadOpened: (phoneNumber: string) => boolean
|
||||
onMessage: (history: History<LocationState>, phoneNumber: string) => void
|
||||
addNewContact: (contact: NewContact) => Promise<string | void>
|
||||
editContact: (contact: Contact) => Promise<string | void>
|
||||
deleteContacts: (ids: ContactID[]) => Promise<string | void>
|
||||
} & Store
|
||||
|
||||
const Phone: FunctionComponent<PhoneProps> = (props) => {
|
||||
@@ -95,6 +98,7 @@ const Phone: FunctionComponent<PhoneProps> = (props) => {
|
||||
onMessage,
|
||||
savingContact,
|
||||
isTopicThreadOpened,
|
||||
resultsState,
|
||||
} = props
|
||||
const history = useHistory()
|
||||
const searchParams = useURLSearchParams()
|
||||
@@ -135,35 +139,39 @@ const Phone: FunctionComponent<PhoneProps> = (props) => {
|
||||
...rest
|
||||
} = useTableSelect<Contact, ContactCategory>(contacts, "contacts")
|
||||
const detailsEnabled = activeRow && !newContact && !editedContact
|
||||
const [resultsState, setResultsState] = useState<ResultsState>(
|
||||
contactList.length === 0 ? ResultsState.Empty : ResultsState.Loaded
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false
|
||||
loadData()
|
||||
}, [])
|
||||
|
||||
const fetchData = async (retried?: boolean) => {
|
||||
setResultsState(ResultsState.Loading)
|
||||
const error = await loadData()
|
||||
const firstRendered = useRef(false)
|
||||
const [retried, setRetried] = useState(false)
|
||||
|
||||
if (cancelled) return
|
||||
setResultsState(ResultsState.Loaded)
|
||||
|
||||
if (error && !retried) {
|
||||
useEffect(() => {
|
||||
if (firstRendered.current) {
|
||||
if (resultsState === ResultsState.Error && !retried) {
|
||||
modalService.openModal(
|
||||
<ErrorWithRetryDataModal onRetry={() => fetchData(true)} />,
|
||||
<ErrorWithRetryDataModal
|
||||
onClose={() => setRetried(true)}
|
||||
onRetry={() => {
|
||||
setRetried(true)
|
||||
loadData()
|
||||
}}
|
||||
/>,
|
||||
true
|
||||
)
|
||||
} else if (resultsState === ResultsState.Error) {
|
||||
modalService.openModal(
|
||||
<ErrorDataModal onClose={() => setRetried(true)} />,
|
||||
true
|
||||
)
|
||||
} else if (error) {
|
||||
modalService.openModal(<ErrorDataModal />, true)
|
||||
}
|
||||
}
|
||||
fetchData()
|
||||
|
||||
return () => {
|
||||
cancelled = true
|
||||
if (!firstRendered.current) {
|
||||
firstRendered.current = true
|
||||
}
|
||||
}, [])
|
||||
}, [resultsState])
|
||||
|
||||
useEffect(() => {
|
||||
setContacts(contactList)
|
||||
|
||||
@@ -6,6 +6,15 @@ import { select } from "Renderer/store"
|
||||
import { RootModel } from "Renderer/models/models"
|
||||
import { URL_MAIN } from "Renderer/constants/urls"
|
||||
import createRouterPath from "Renderer/utils/create-router-path"
|
||||
import {
|
||||
Contact,
|
||||
ContactID,
|
||||
NewContact,
|
||||
} from "Renderer/models/phone/phone.typings"
|
||||
import addContact from "Renderer/requests/add-contact.request"
|
||||
import logger from "App/main/utils/logger"
|
||||
import editContact from "Renderer/requests/edit-contact.request"
|
||||
import deleteContactsRequest from "Renderer/requests/delete-contacts.request"
|
||||
|
||||
const selector = select(({ phone, messages }) => ({
|
||||
contactList: phone.contactList,
|
||||
@@ -39,8 +48,35 @@ const mapDispatch = ({ phone, auth }: any) => {
|
||||
history.push(createRouterPath(URL_MAIN.messages, { phoneNumber })),
|
||||
onSpeedDialSettingsSave: noop,
|
||||
loadData: phone.loadData,
|
||||
addNewContact: phone.addNewContact,
|
||||
editContact: phone.editContact,
|
||||
addNewContact: async (contact: NewContact): Promise<string | void> => {
|
||||
const { data, error } = await addContact(contact)
|
||||
if (error || !data) {
|
||||
logger.error(error)
|
||||
return error?.message ?? "Something went wrong"
|
||||
} else {
|
||||
phone.addContact(data)
|
||||
}
|
||||
},
|
||||
editContact: async (contact: Contact): Promise<string | void> => {
|
||||
const { data, error } = await editContact(contact)
|
||||
if (error || !data) {
|
||||
logger.error(error)
|
||||
return error?.message ?? "Something went wrong"
|
||||
} else {
|
||||
phone.updateContact(data)
|
||||
}
|
||||
},
|
||||
deleteContacts: async (ids: ContactID[]): Promise<string | void> => {
|
||||
const { error } = await deleteContactsRequest(ids)
|
||||
if (error) {
|
||||
logger.error(error)
|
||||
const successIds = ids.filter((id) => !error.data?.includes(id))
|
||||
phone.removeContact(successIds)
|
||||
return error?.message ?? "Something went wrong"
|
||||
} else {
|
||||
phone.removeContact(ids)
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user