diff --git a/run/localizer/constants.ts b/run/localizer/constants.ts index 8ade63e5..7ff7fec1 100644 --- a/run/localizer/constants.ts +++ b/run/localizer/constants.ts @@ -3,6 +3,13 @@ export enum LOCALE_DEFAULTS { session_download_url = 'https://getsession.org/download', gif = 'GIF', oxen_foundation = 'Oxen Foundation', + network_name = 'Session Network', + token_name_long = 'Session Token', + staking_reward_pool = 'Staking Reward Pool', + token_name_short = 'SESH', + usd_name_short = 'USD', + session_network_data_price = 'Price data powered by CoinGecko
Accurate at {date_time}', + app_pro = 'Session Pro', } export const rtlLocales = ['ar', 'fa', 'he', 'ps', 'ur']; diff --git a/run/localizer/locales.ts b/run/localizer/locales.ts index 871ebc60..2be47598 100644 --- a/run/localizer/locales.ts +++ b/run/localizer/locales.ts @@ -14,6 +14,10 @@ export const simpleDictionary = { en: 'Copy Account ID', args: undefined, }, + accountId: { + en: 'Account ID', + args: undefined, + }, accountIdCopied: { en: 'Account ID Copied', args: undefined, @@ -58,6 +62,14 @@ export const simpleDictionary = { en: 'Add', args: undefined, }, + addAdmins: { + en: 'Add Admins', + args: undefined, + }, + addAdminsDescription: { + en: 'Enter the Account ID of the user you are promoting to admin.

To add multiple users, enter each Account ID separated by a comma. Up to 20 Account IDs can be specified at a time.', + args: undefined, + }, adminCannotBeRemoved: { en: 'Admins cannot be removed.', args: undefined, @@ -174,10 +186,22 @@ export const simpleDictionary = { en: 'App Icon', args: undefined, }, + appIconAndNameChange: { + en: 'Change App Icon and Name', + args: undefined, + }, + appIconAndNameChangeConfirmation: { + en: 'Changing the app icon and name requires Session to be closed. Notifications will continue to use the default Session icon and name.', + args: undefined, + }, appIconAndNameDescription: { en: 'Alternate app icon and name is displayed on home screen and app drawer.', args: undefined, }, + appIconAndNameSelectionDescription: { + en: 'The selected app icon and name is displayed on the home screen and app drawer.', + args: undefined, + }, appIconAndNameSelectionTitle: { en: 'Icon and name', args: undefined, @@ -542,6 +566,10 @@ export const simpleDictionary = { en: 'Unban User', args: undefined, }, + banUnbanUserDescription: { + en: 'Enter the Account ID of the user you are unbanning', + args: undefined, + }, banUnbanUserUnbanned: { en: 'User unbanned', args: undefined, @@ -554,12 +582,16 @@ export const simpleDictionary = { en: 'User banned', args: undefined, }, + banUserDescription: { + en: 'Enter the Account ID of the user you are banning', + args: undefined, + }, block: { en: 'Block', args: undefined, }, blockBlockedDescription: { - en: 'Unblock this contact to send a message.', + en: 'Unblock this contact to send a message', args: undefined, }, blockBlockedNone: { @@ -782,6 +814,14 @@ export const simpleDictionary = { en: 'Clear device only', args: undefined, }, + clearDeviceRestart: { + en: 'Clear Device and Restart', + args: undefined, + }, + clearDeviceRestore: { + en: 'Clear Device and Restore', + args: undefined, + }, clearMessages: { en: 'Clear All Messages', args: undefined, @@ -790,10 +830,18 @@ export const simpleDictionary = { en: 'Are you sure you want to clear all messages from your conversation with {name} from your device?', args: { name: 'string' }, }, + clearMessagesChatDescriptionUpdated: { + en: 'Are you sure you want to clear all messages from your conversation with {name} on this device?', + args: { name: 'string' }, + }, clearMessagesCommunity: { en: 'Are you sure you want to clear all {community_name} messages from your device?', args: { community_name: 'string' }, }, + clearMessagesCommunityUpdated: { + en: 'Are you sure you want to clear all messages from {community_name} on this device?', + args: { community_name: 'string' }, + }, clearMessagesForEveryone: { en: 'Clear for everyone', args: undefined, @@ -806,18 +854,38 @@ export const simpleDictionary = { en: 'Are you sure you want to clear all {group_name} messages?', args: { group_name: 'string' }, }, + clearMessagesGroupAdminDescriptionUpdated: { + en: 'Are you sure you want to clear all messages from {group_name}?', + args: { group_name: 'string' }, + }, clearMessagesGroupDescription: { en: 'Are you sure you want to clear all {group_name} messages from your device?', args: { group_name: 'string' }, }, + clearMessagesGroupDescriptionUpdated: { + en: 'Are you sure you want to clear all messages from {group_name} on this device?', + args: { group_name: 'string' }, + }, clearMessagesNoteToSelfDescription: { en: 'Are you sure you want to clear all Note to Self messages from your device?', args: undefined, }, + clearMessagesNoteToSelfDescriptionUpdated: { + en: 'Are you sure you want to clear all Note to Self messages on this device?', + args: undefined, + }, + clearOnThisDevice: { + en: 'Clear on this device', + args: undefined, + }, close: { en: 'Close', args: undefined, }, + closeApp: { + en: 'Close App', + args: undefined, + }, closeWindow: { en: 'Close Window', args: undefined, @@ -1082,8 +1150,16 @@ export const simpleDictionary = { en: 'Cut', args: undefined, }, + databaseErrorClearDataWarning: { + en: 'Are you sure you want to delete all messages, attachments, and account data from this device and create a new account?', + args: undefined, + }, databaseErrorGeneric: { - en: 'A database error occurred.

Export your application logs to share for troubleshooting. If this is unsuccessful, reinstall Session and restore your account.

Warning: This will result in loss of all messages, attachments, and account data older than two weeks.', + en: 'A database error occurred.

Export your application logs to share for troubleshooting. If this is unsuccessful, reinstall Session and restore your account.', + args: undefined, + }, + databaseErrorRestoreDataWarning: { + en: 'Are you sure you want to delete all messages, attachments, and account data from this device and restore your account from the network?', args: undefined, }, databaseErrorTimeout: { @@ -1170,6 +1246,14 @@ export const simpleDictionary = { en: 'You don’t have permission to delete others’ messages', args: undefined, }, + deleteContactDescription: { + en: 'Are you sure you want to delete {name} from your contacts?

This will delete your conversation, including all messages and attachments. Future messages from {name} will appear as a message request.', + args: { name: 'string' }, + }, + deleteConversationDescription: { + en: 'Are you sure you want to delete your conversation with {name}?
This will permanently delete all messages and attachments.', + args: { name: 'string' }, + }, deleteMessageDeletedGlobally: { en: 'This message was deleted', args: undefined, @@ -1386,6 +1470,10 @@ export const simpleDictionary = { en: 'Document', args: undefined, }, + donate: { + en: 'Donate', + args: undefined, + }, done: { en: 'Done', args: undefined, @@ -1498,6 +1586,10 @@ export const simpleDictionary = { en: 'Database Error', args: undefined, }, + errorGeneric: { + en: 'Something went wrong. Please try again later.', + args: undefined, + }, errorUnknown: { en: 'An unknown error occurred.', args: undefined, @@ -1522,6 +1614,10 @@ export const simpleDictionary = { en: 'Follow system settings', args: undefined, }, + forever: { + en: 'Forever', + args: undefined, + }, from: { en: 'From:', args: undefined, @@ -1559,7 +1655,7 @@ export const simpleDictionary = { args: undefined, }, groupDeleteDescription: { - en: 'Are you sure you want to delete {group_name}? This will remove all members and delete all group content.', + en: 'Are you sure you want to delete {group_name}?

This will remove all members and delete all group content.', args: { group_name: 'string' }, }, groupDeleteDescriptionMember: { @@ -1767,7 +1863,7 @@ export const simpleDictionary = { args: { group_name: 'string' }, }, groupNotUpdatedWarning: { - en: 'Group has not been updated in over 30 days. You may experience issues sending messages or viewing Group information.', + en: 'This group has not been updated in over 30 days. You may experience issues sending messages or viewing group information.', args: undefined, }, groupOnlyAdmin: { @@ -1894,6 +1990,10 @@ export const simpleDictionary = { en: 'Toggle system menu bar visibility', args: undefined, }, + hideNoteToSelfDescription: { + en: 'Are you sure you want to hide Note to Self from your conversation list?', + args: undefined, + }, hideOthers: { en: 'Hide Others', args: undefined, @@ -2166,6 +2266,14 @@ export const simpleDictionary = { en: 'Replying to', args: undefined, }, + messageRequestDisabledToastAttachments: { + en: 'You cannot send attachments until your Message Request is accepted', + args: undefined, + }, + messageRequestDisabledToastVoiceMessages: { + en: 'You cannot send voice messages until your Message Request is accepted', + args: undefined, + }, messageRequestGroupInvite: { en: '{name} invited you to join {group_name}.', args: { name: 'string', group_name: 'string' }, @@ -2206,6 +2314,10 @@ export const simpleDictionary = { en: 'Allow message requests from Community conversations.', args: undefined, }, + messageRequestsContactDelete: { + en: 'Are you sure you want to delete this message request and the associated contact?', + args: undefined, + }, messageRequestsDelete: { en: 'Are you sure you want to delete this message request?', args: undefined, @@ -2274,6 +2386,14 @@ export const simpleDictionary = { en: 'Minimize', args: undefined, }, + modalMessageTooLongDescription: { + en: 'Please shorten your message to {count} characters or less.', + args: { count: 'number' }, + }, + modalMessageTooLongTitle: { + en: 'Your message is too long', + args: undefined, + }, next: { en: 'Next', args: undefined, @@ -2426,6 +2546,14 @@ export const simpleDictionary = { en: 'Muted', args: undefined, }, + notificationsMutedFor: { + en: 'Muted for {time_large}', + args: { time_large: 'string' }, + }, + notificationsMutedForTime: { + en: 'Muted until {date_time}', + args: { date_time: 'string' }, + }, notificationsSlowMode: { en: 'Slow Mode', args: undefined, @@ -2790,6 +2918,10 @@ export const simpleDictionary = { en: 'Session needs storage access to send photos and videos.', args: undefined, }, + permissionsWriteCommunity: { + en: "You don't have write permissions in this community", + args: undefined, + }, pin: { en: 'Pin', args: undefined, @@ -2982,6 +3114,14 @@ export const simpleDictionary = { en: 'Redo', args: undefined, }, + remainingCharactersOverTooltip: { + en: 'Message is too long', + args: undefined, + }, + remainingCharactersTooltip: { + en: '{count} characters remaining', + args: { count: 'number' }, + }, remove: { en: 'Remove', args: undefined, @@ -3090,6 +3230,10 @@ export const simpleDictionary = { en: 'Select All', args: undefined, }, + selectAppIcon: { + en: 'Select app icon', + args: undefined, + }, send: { en: 'Send', args: undefined, @@ -3134,6 +3278,46 @@ export const simpleDictionary = { en: 'Message Requests', args: undefined, }, + sessionNetworkCurrentPrice: { + en: 'Current SESH price', + args: undefined, + }, + sessionNetworkDescription: { + en: 'Messages are sent using the Session Network. The network is comprised of nodes incentivized with Session Token, which keeps Session decentralized and secure. Learn More {icon}', + args: { icon: 'string' }, + }, + sessionNetworkLearnAboutStaking: { + en: 'Learn About Staking', + args: undefined, + }, + sessionNetworkMarketCap: { + en: 'Market Cap', + args: undefined, + }, + sessionNetworkNodesSecuring: { + en: 'Session Nodes securing your messages', + args: undefined, + }, + sessionNetworkNodesSwarm: { + en: 'Session Nodes in your swarm', + args: undefined, + }, + sessionNetworkNotificationLive: { + en: 'Session Token is live! Explore the new Session Network section in Settings to learn how Session Token powers Session.', + args: undefined, + }, + sessionNetworkSecuredBy: { + en: 'Network secured by', + args: undefined, + }, + sessionNetworkTokenDescription: { + en: 'When you stake Session Token to secure the network, you earn rewards in SESH from the Staking Reward Pool.', + args: undefined, + }, + sessionNew: { + en: ' New', + args: undefined, + }, sessionNotifications: { en: 'Notifications', args: undefined, @@ -3158,6 +3342,10 @@ export const simpleDictionary = { en: 'Set', args: undefined, }, + setCommunityDisplayPicture: { + en: 'Set Community Display Picture', + args: undefined, + }, settingsRestartDescription: { en: 'You must restart Session to apply your new settings.', args: undefined, @@ -3198,6 +3386,14 @@ export const simpleDictionary = { en: 'Show Less', args: undefined, }, + showNoteToSelf: { + en: 'Show Note to Self', + args: undefined, + }, + showNoteToSelfDescription: { + en: 'Are you sure you want to show Note to Self in your conversation list?', + args: undefined, + }, stickers: { en: 'Stickers', args: undefined, @@ -3238,6 +3434,10 @@ export const simpleDictionary = { en: 'See and share typing indicators.', args: undefined, }, + unavailable: { + en: 'Unavailable', + args: undefined, + }, undo: { en: 'Undo', args: undefined, @@ -3266,6 +3466,18 @@ export const simpleDictionary = { en: 'Session failed to update. Please go to https://getsession.org/download and install the new version manually, then contact our Help Center to let us know about this problem.', args: undefined, }, + updateGroupInformation: { + en: 'Update Group Information', + args: undefined, + }, + updateGroupInformationDescription: { + en: 'Group name and description are visible to all group members.', + args: undefined, + }, + updateGroupInformationEnterShorterDescription: { + en: 'Please enter a shorter group description', + args: undefined, + }, updateNewVersion: { en: 'A new version of Session is available, tap to update', args: undefined, @@ -3286,6 +3498,10 @@ export const simpleDictionary = { en: 'Version {version}', args: { version: 'string' }, }, + updated: { + en: 'Last updated {relative_time} ago', + args: { relative_time: 'string' }, + }, uploading: { en: 'Uploading', args: undefined, diff --git a/run/screenshots/android/app_disguise.png b/run/screenshots/android/app_disguise.png new file mode 100644 index 00000000..95cf431e --- /dev/null +++ b/run/screenshots/android/app_disguise.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:18ed347a459550771454d6038a4a4eb2d52792f0392ece8e8dace5c919251007 +size 127457 diff --git a/run/screenshots/ios/app_disguise.png b/run/screenshots/ios/app_disguise.png new file mode 100644 index 00000000..a17bbfea --- /dev/null +++ b/run/screenshots/ios/app_disguise.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6987b366a695c5b9b39855e95230b0965928f4c7a6f7e9d090f2cf686191d8e0 +size 454344 diff --git a/run/test/specs/app_disguise_icons.spec.ts b/run/test/specs/app_disguise_icons.spec.ts new file mode 100644 index 00000000..538fc98c --- /dev/null +++ b/run/test/specs/app_disguise_icons.spec.ts @@ -0,0 +1,30 @@ +import { bothPlatformsIt } from '../../types/sessionIt'; +import { SupportedPlatformsType, closeApp, openAppOnPlatformSingleDevice } from './utils/open_app'; +import { newUser } from './utils/create_account'; +import { USERNAME } from '../../types/testing'; +import { AppearanceMenuItem, SelectAppIcon, UserSettings } from './locators/settings'; +import { verifyElementScreenshot } from './utils/verify_screenshots'; +import { AppDisguisePageScreenshot } from './utils/screenshot_paths'; +import { sleepFor } from './utils'; + +bothPlatformsIt({ + title: 'App disguise icons', + risk: 'medium', + countOfDevicesNeeded: 1, + testCb: appDisguiseIcons, +}); + +async function appDisguiseIcons(platform: SupportedPlatformsType) { + const { device } = await openAppOnPlatformSingleDevice(platform); + await newUser(device, USERNAME.ALICE); + await device.clickOnElementAll(new UserSettings(device)); + // Must scroll down to reveal the Appearance menu item + await device.scrollDown(); + await device.clickOnElementAll(new AppearanceMenuItem(device)); + await sleepFor(2000); + // Must scroll down to reveal the app disguise option + await device.scrollDown(); + await device.clickOnElementAll(new SelectAppIcon(device)); + await verifyElementScreenshot(device, new AppDisguisePageScreenshot(device)); + await closeApp(device); +} diff --git a/run/test/specs/app_disguise_set.spec.ts b/run/test/specs/app_disguise_set.spec.ts new file mode 100644 index 00000000..cdb37059 --- /dev/null +++ b/run/test/specs/app_disguise_set.spec.ts @@ -0,0 +1,57 @@ +import { androidIt } from '../../types/sessionIt'; +import { SupportedPlatformsType, openAppOnPlatformSingleDevice } from './utils/open_app'; +import { newUser } from './utils/create_account'; +import { USERNAME } from '../../types/testing'; +import { + AppDisguiseMeetingIcon, + AppearanceMenuItem, + CloseAppButton, + SelectAppIcon, + UserSettings, +} from './locators/settings'; +import { DisguisedApp } from './locators/external'; +import { sleepFor } from './utils'; +import { runScriptAndLog } from './utils/utilities'; +import { getAdbFullPath } from './utils/binaries'; +import { closeApp } from './utils/open_app'; +import { englishStrippedStr } from '../../localizer/englishStrippedStr'; + +// iOS implementation blocked by SES-3809 +androidIt({ + title: 'App disguise set icon', + risk: 'medium', + countOfDevicesNeeded: 1, + testCb: appDisguiseSetIcon, +}); + +async function appDisguiseSetIcon(platform: SupportedPlatformsType) { + const { device } = await openAppOnPlatformSingleDevice(platform); + await newUser(device, USERNAME.ALICE); + await device.clickOnElementAll(new UserSettings(device)); + // Must scroll down to reveal the Appearance menu item + await device.scrollDown(); + await device.clickOnElementAll(new AppearanceMenuItem(device)); + await sleepFor(2000); + // Must scroll down to reveal the app disguise option + await device.scrollDown(); + await device.clickOnElementAll(new SelectAppIcon(device)); + try { + await device.clickOnElementAll(new AppDisguiseMeetingIcon(device)); + await device.checkModalStrings( + englishStrippedStr('appIconAndNameChange').toString(), + englishStrippedStr('appIconAndNameChangeConfirmation').toString() + ); + await device.clickOnElementAll(new CloseAppButton(device)); + await sleepFor(2000); + // // Open app library and check for disguised app + await device.swipeFromBottom(); + await device.waitForTextElementToBePresent(new DisguisedApp(device)); + } finally { + // The disguised app must be uninstalled otherwise every following test will fail + await closeApp(device); + await runScriptAndLog( + `${getAdbFullPath()} -s ${device.udid} uninstall network.loki.messenger`, + true + ); + } +} diff --git a/run/test/specs/locators/external.ts b/run/test/specs/locators/external.ts index 1145b2f7..eeb9080c 100644 --- a/run/test/specs/locators/external.ts +++ b/run/test/specs/locators/external.ts @@ -8,3 +8,13 @@ export class PhotoLibrary extends LocatorsInterface { } as const; } } + +export class DisguisedApp extends LocatorsInterface { + public build() { + return { + strategy: 'accessibility id', + selector: 'MeetingSE', + maxWait: 5000, + } as const; + } +} diff --git a/run/test/specs/locators/settings.ts b/run/test/specs/locators/settings.ts index 2a80fb96..0a05e42c 100644 --- a/run/test/specs/locators/settings.ts +++ b/run/test/specs/locators/settings.ts @@ -104,3 +104,78 @@ export class BlockedContacts extends LocatorsInterface { } } } + +export class AppearanceMenuItem extends LocatorsInterface { + public build() { + switch (this.platform) { + case 'android': + case 'ios': + return { + strategy: 'accessibility id', + selector: 'Appearance', + } as const; + } + } +} + +export class SelectAppIcon extends LocatorsInterface { + public build() { + switch (this.platform) { + case 'android': + return { + strategy: 'id', + selector: 'network.loki.messenger:id/system_settings_app_icon', + } as const; + case 'ios': + return { + strategy: 'accessibility id', + selector: 'Select alternate app icon', + } as const; + } + } +} +export class AppDisguisePage extends LocatorsInterface { + public build() { + switch (this.platform) { + case 'android': + return { + strategy: 'class name', + selector: 'android.widget.ScrollView', + } as const; + case 'ios': + return { + strategy: 'class name', + selector: 'XCUIElementTypeTable', + } as const; + } + } +} +export class AppDisguiseMeetingIcon extends LocatorsInterface { + public build() { + switch (this.platform) { + case 'android': + return { + strategy: 'id', + selector: 'MeetingSE option', + } as const; + case 'ios': + // NOTE see SES-3809 + throw new Error('No locators implemented for iOS'); + } + } +} + +export class CloseAppButton extends LocatorsInterface { + public build() { + switch (this.platform) { + case 'android': + return { + strategy: 'class name', + selector: 'android.widget.TextView', + text: 'Close App', + } as const; + case 'ios': + throw new Error('Modal not implemented for iOS'); + } + } +} diff --git a/run/test/specs/utils/screenshot_paths.ts b/run/test/specs/utils/screenshot_paths.ts index 5d750bb8..8ea2c27e 100644 --- a/run/test/specs/utils/screenshot_paths.ts +++ b/run/test/specs/utils/screenshot_paths.ts @@ -2,6 +2,7 @@ import path from 'path'; import { EmptyLandingPage } from '../locators/home'; import { SupportedPlatformsType } from './open_app'; import { PageName } from '../../../types/testing'; +import { AppDisguisePage } from '../locators/settings'; // Extends locator classes with baseline screenshot paths for visual regression testing // If a locator appears in multiple states, a state argument must be provided to screenshotFileName() @@ -18,3 +19,9 @@ export class BrowserPageScreenshot { return path.join('run', 'screenshots', platform, `browser_${pageName}.png`); } } + +export class AppDisguisePageScreenshot extends AppDisguisePage { + public screenshotFileName(): string { + return path.join('run', 'screenshots', this.platform, 'app_disguise.png'); + } +} diff --git a/run/types/DeviceWrapper.ts b/run/types/DeviceWrapper.ts index 41dfb15b..34db3153 100644 --- a/run/types/DeviceWrapper.ts +++ b/run/types/DeviceWrapper.ts @@ -1587,6 +1587,11 @@ export class DeviceWrapper { await this.scroll({ x: 760, y: 710 }, { x: 760, y: 1500 }, 100); } + public async swipeFromBottom(): Promise { + const { width, height } = await this.getWindowRect(); + + await this.scroll({ x: width / 2, y: height * 0.95 }, { x: width / 2, y: height * 0.35 }, 100); + } public async scrollToBottom() { if (this.isAndroid()) { const scrollButton = await this.doesElementExist({ diff --git a/run/types/testing.ts b/run/types/testing.ts index deaab229..cacb68c1 100644 --- a/run/types/testing.ts +++ b/run/types/testing.ts @@ -361,7 +361,10 @@ export type AccessibilityId = | 'Learn about staking link' | 'Last updated timestamp' | 'Albums' - | 'Select'; + | 'Select' + | 'Appearance' + | 'Select alternate app icon' + | 'MeetingSE'; export type Id = | 'Modal heading' @@ -433,7 +436,8 @@ export type Id = | 'session-network-menu-item' | 'Last updated timestamp' | 'Image button' - | 'android.widget.TextView'; + | 'network.loki.messenger:id/system_settings_app_icon' + | 'MeetingSE option'; export type TestRisk = 'high' | 'medium' | 'low';