Merge branch 'master' into fix-auto-commit

This commit is contained in:
Daniel Karski
2020-12-08 10:16:40 +01:00
12 changed files with 156 additions and 106 deletions

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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} />

View File

@@ -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>
)

View File

@@ -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
}

View File

@@ -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

View File

@@ -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,
})

View File

@@ -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() {

View File

@@ -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>
}

View File

@@ -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)

View File

@@ -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)
}
},
}
}