diff --git a/src/components/views/dialogs/QuestionDialog.tsx b/src/components/views/dialogs/QuestionDialog.tsx index a0389f75e3..b1e913dc8e 100644 --- a/src/components/views/dialogs/QuestionDialog.tsx +++ b/src/components/views/dialogs/QuestionDialog.tsx @@ -23,7 +23,7 @@ import { IDialogProps } from "./IDialogProps"; import BaseDialog from "./BaseDialog"; import DialogButtons from "../elements/DialogButtons"; -interface IProps extends IDialogProps { +export interface IQuestionDialogProps extends IDialogProps { title?: string; description?: React.ReactNode; extraButtons?: React.ReactNode; @@ -39,8 +39,8 @@ interface IProps extends IDialogProps { cancelButton?: React.ReactNode; } -export default class QuestionDialog extends React.Component { - public static defaultProps: Partial = { +export default class QuestionDialog extends React.Component { + public static defaultProps: Partial = { title: "", description: "", extraButtons: null, diff --git a/src/components/views/location/shareLocation.ts b/src/components/views/location/shareLocation.ts index 0ff3098501..ca738d73c2 100644 --- a/src/components/views/location/shareLocation.ts +++ b/src/components/views/location/shareLocation.ts @@ -15,6 +15,7 @@ limitations under the License. */ import { MatrixClient } from "matrix-js-sdk/src/client"; +import { MatrixError } from "matrix-js-sdk/src/http-api"; import { makeLocationContent, makeBeaconInfoContent } from "matrix-js-sdk/src/content-helpers"; import { logger } from "matrix-js-sdk/src/logger"; import { IEventRelation } from "matrix-js-sdk/src/models/event"; @@ -23,7 +24,7 @@ import { THREAD_RELATION_TYPE } from "matrix-js-sdk/src/models/thread"; import { _t } from "../../../languageHandler"; import Modal from "../../../Modal"; -import QuestionDialog from "../dialogs/QuestionDialog"; +import QuestionDialog, { IQuestionDialogProps } from "../dialogs/QuestionDialog"; import SdkConfig from "../../../SdkConfig"; import { OwnBeaconStore } from "../../../stores/OwnBeaconStore"; import { doMaybeLocalRoomAction } from "../../../utils/local-room"; @@ -45,12 +46,32 @@ const DEFAULT_LIVE_DURATION = 300000; export type ShareLocationFn = (props: LocationShareProps) => Promise; -const handleShareError = (error: Error, openMenu: () => void, shareType: LocationShareType) => { - const errorMessage = shareType === LocationShareType.Live ? - "We couldn't start sharing your live location" : - "We couldn't send your location"; - logger.error(errorMessage, error); - const params = { +const getPermissionsErrorParams = (shareType: LocationShareType): { + errorMessage: string; + modalParams: IQuestionDialogProps; +} => { + const errorMessage = shareType === LocationShareType.Live + ? "Insufficient permissions to start sharing your live location" + : "Insufficient permissions to send your location"; + + const modalParams = { + title: _t("You don't have permission to share locations"), + description: _t("You need to have the right permissions in order to share locations in this room."), + button: _t("OK"), + hasCancelButton: false, + onFinished: () => {}, // NOOP + }; + return { modalParams, errorMessage }; +}; + +const getDefaultErrorParams = (shareType: LocationShareType, openMenu: () => void): { + errorMessage: string; + modalParams: IQuestionDialogProps; +} => { + const errorMessage = shareType === LocationShareType.Live + ? "We couldn't start sharing your live location" + : "We couldn't send your location"; + const modalParams = { title: _t("We couldn't send your location"), description: _t("%(brand)s could not send your location. Please try again later.", { brand: SdkConfig.get().brand, @@ -63,7 +84,17 @@ const handleShareError = (error: Error, openMenu: () => void, shareType: Locatio } }, }; - Modal.createDialog(QuestionDialog, params); + return { modalParams, errorMessage }; +}; + +const handleShareError = (error: Error, openMenu: () => void, shareType: LocationShareType): void => { + const { modalParams, errorMessage } = (error as MatrixError).errcode === 'M_FORBIDDEN' ? + getPermissionsErrorParams(shareType) : + getDefaultErrorParams(shareType, openMenu); + + logger.error(errorMessage, error); + + Modal.createDialog(QuestionDialog, modalParams); }; export const shareLiveLocation = ( diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 2d5b4d52b9..ad82fd4318 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -2199,6 +2199,8 @@ "Failed to fetch your location. Please try again later.": "Failed to fetch your location. Please try again later.", "Timed out trying to fetch your location. Please try again later.": "Timed out trying to fetch your location. Please try again later.", "Unknown error fetching location. Please try again later.": "Unknown error fetching location. Please try again later.", + "You don't have permission to share locations": "You don't have permission to share locations", + "You need to have the right permissions in order to share locations in this room.": "You need to have the right permissions in order to share locations in this room.", "We couldn't send your location": "We couldn't send your location", "%(brand)s could not send your location. Please try again later.": "%(brand)s could not send your location. Please try again later.", "%(displayName)s's live location": "%(displayName)s's live location", diff --git a/test/components/views/location/LocationShareMenu-test.tsx b/test/components/views/location/LocationShareMenu-test.tsx index 7e41f3f149..221bbb1917 100644 --- a/test/components/views/location/LocationShareMenu-test.tsx +++ b/test/components/views/location/LocationShareMenu-test.tsx @@ -41,6 +41,7 @@ import Modal from '../../../../src/Modal'; import { DEFAULT_DURATION_MS } from '../../../../src/components/views/location/LiveDurationDropdown'; import { OwnBeaconStore } from '../../../../src/stores/OwnBeaconStore'; import { SettingLevel } from '../../../../src/settings/SettingLevel'; +import QuestionDialog from '../../../../src/components/views/dialogs/QuestionDialog'; jest.useFakeTimers(); @@ -417,7 +418,7 @@ describe('', () => { })); }); - it('opens error dialog when beacon creation fails ', async () => { + it('opens error dialog when beacon creation fails', async () => { // stub logger to keep console clean from expected error const logSpy = jest.spyOn(logger, 'error').mockReturnValue(undefined); const error = new Error('oh no'); @@ -438,7 +439,41 @@ describe('', () => { await flushPromisesWithFakeTimers(); expect(logSpy).toHaveBeenCalledWith("We couldn't start sharing your live location", error); - expect(mocked(Modal).createDialog).toHaveBeenCalled(); + expect(mocked(Modal).createDialog).toHaveBeenCalledWith(QuestionDialog, expect.objectContaining({ + button: 'Try again', + description: 'Element could not send your location. Please try again later.', + title: `We couldn't send your location`, + cancelButton: 'Cancel', + })); + }); + + it('opens error dialog when beacon creation fails with permission error', async () => { + // stub logger to keep console clean from expected error + const logSpy = jest.spyOn(logger, 'error').mockReturnValue(undefined); + const error = { errcode: 'M_FORBIDDEN' } as unknown as Error; + mockClient.unstable_createLiveBeacon.mockRejectedValue(error); + const component = getComponent(); + + // advance to location picker + setShareType(component, LocationShareType.Live); + setLocation(component); + + act(() => { + getSubmitButton(component).at(0).simulate('click'); + component.setProps({}); + }); + + await flushPromisesWithFakeTimers(); + await flushPromisesWithFakeTimers(); + await flushPromisesWithFakeTimers(); + + expect(logSpy).toHaveBeenCalledWith("Insufficient permissions to start sharing your live location", error); + expect(mocked(Modal).createDialog).toHaveBeenCalledWith(QuestionDialog, expect.objectContaining({ + button: 'OK', + description: 'You need to have the right permissions in order to share locations in this room.', + title: `You don't have permission to share locations`, + hasCancelButton: false, + })); }); }); });