diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 62a2ea70..83212e98 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -1,5 +1,5 @@ --- -name: Bug report +name: Bug report (lexiconhq) about: Create a report to help us improve title: '' labels: '' @@ -17,9 +17,9 @@ assignees: '' --- ### Describe the bug -A clear and concise description of what the bug is. +_A clear and concise description of what the bug is._ -### *REQUIRED:* Include the contents of `api/.env` and `frontend/.env` +### *REQUIRED:* Include the contents of `api/.env` and `frontend/Config.ts` _If you are manually overriding any environment variables when running the package scripts, include those as well._ **api/.env** @@ -33,22 +33,19 @@ _If you are manually overriding any environment variables when running the packa ``` ### _REQUIRED:_ answer the following questions: -- If this is related to your mobile app: - - Are you running it through Expo, or did you build it? - - Is it running on your mobile device or on a simulator? +- _If this is related to your mobile app:_ + - _Are you running it through Expo, or did you build it?_ + - _Is it running on your mobile device or on a simulator?_ ### To Reproduce -Steps to reproduce the behavior: -1. Go to '...' -2. Click on '....' -3. Scroll down to '....' -4. See error +_Steps to reproduce the behavior_ +***Please use a numbered list** ### Expected behavior -A clear and concise description of what you expected to happen. +_A clear and concise description of what you expected to happen._ ### Screenshots -If applicable, add screenshots to help explain your problem. +_If applicable, add screenshots to help explain your problem._ ### Environments @@ -64,13 +61,13 @@ If applicable, add screenshots to help explain your problem. - Version [e.g. 22] #### GraphQL API (please complete the following information): -Please indicate where the Prose GraphQL API is running. +_Please indicate where the Prose GraphQL API is running._ -Are you only running it locally on your development machine? +- _Are you only running it locally on your development machine?_ -Have you deployed it somewhere? If so, what domain or IP address did you deploy it at? +- _Have you deployed it somewhere? If so, what domain or IP address did you deploy it at?_ -How specifically did you configure the mobile app to connect to the API? +- _How specifically did you configure the mobile app to connect to the API?_ ### Additional context -Add any other context about the problem here. +_Add any other context about the problem here._ diff --git a/.github/ISSUE_TEMPLATE/new_feature.md b/.github/ISSUE_TEMPLATE/new_feature.md new file mode 100644 index 00000000..4e795d2a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/new_feature.md @@ -0,0 +1,15 @@ +--- +name: New Feature +about: A new Lexicon feature +title: '' +labels: '' +assignees: '' + +--- + +## Description + +## Acceptance Criteria +- [ ] + +## Guidance diff --git a/.github/workflows/delete-build-jet-cache.yml b/.github/workflows/delete-build-jet-cache.yml new file mode 100644 index 00000000..ccba2d98 --- /dev/null +++ b/.github/workflows/delete-build-jet-cache.yml @@ -0,0 +1,18 @@ +name: Manually Delete BuildJet Cache + +on: + workflow_dispatch: + inputs: + cache_key: + description: 'BuildJet Cache Key to Delete' + required: true + type: string +jobs: + manually-delete-buildjet-cache: + runs-on: buildjet-2vcpu-ubuntu-2204 + steps: + - name: Checkout + uses: actions/checkout@v3 + - uses: buildjet/cache-delete@v1 + with: + cache_key: ${{ inputs.cache_key }} diff --git a/.github/workflows/e2e-test-android.yml b/.github/workflows/e2e-test-android.yml new file mode 100644 index 00000000..20052c6e --- /dev/null +++ b/.github/workflows/e2e-test-android.yml @@ -0,0 +1,111 @@ +name: E2E test android + +on: workflow_call + +jobs: + e2e-test-android: + runs-on: ${{matrix.runs-on}} + name: Android Test ${{matrix.runs-on}} + strategy: + fail-fast: false + matrix: + runs-on: [buildjet-4vcpu-ubuntu-2204] + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - uses: buildjet/setup-node@v4 + with: + node-version: '20.x' + + - name: Configure JDK + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '11' + + - name: Get yarn cache directory path + id: yarn-cache-dir-path + run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT + + - uses: buildjet/cache@v4 + id: yarn-cache + with: + path: ${{ steps.yarn-cache-dir-path.outputs.dir }} + key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} + restore-keys: | + ${{ runner.os }}-yarn- + + - name: Install dependencies + run: yarn + + - name: Generate files + run: yarn generate + + - name: Cache Detox build + id: cache-detox-build + uses: buildjet/cache@v4 + with: + path: frontend/android + key: ${{ runner.os }}-detox-build + + - name: Expo prebuild android and build detox + if: steps.cache-detox-build.outputs.cache-hit != 'true' + run: | + cd frontend + npx expo prebuild --platform android + yarn tests:android:build + + - name: Setup Detox + run: | + cd frontend + npm install -g detox-cli + detox clean-framework-cache && yarn detox build-framework-cache + + - name: Gradle cache + uses: gradle/gradle-build-action@v2 + + - name: AVD cache + uses: buildjet/cache@v3 + id: avd-cache + with: + path: | + ~/.android/avd/* + ~/.android/adb* + key: avd-28 + + - name: create AVD and generate snapshot for caching + if: steps.avd-cache.outputs.cache-hit != 'true' + uses: reactivecircus/android-emulator-runner@v2 + with: + api-level: 28 + target: google_apis + arch: x86 + profile: pixel_5 + avd-name: Pixel_5_API_28 + force-avd-creation: false + emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none + disable-animations: false + script: echo "Generated AVD snapshot for caching." + + - name: Run Test + timeout-minutes: 25 + uses: reactivecircus/android-emulator-runner@v2 + with: + api-level: 28 + target: google_apis + arch: x86 + profile: pixel_5 + avd-name: Pixel_5_API_28 + force-avd-creation: false + emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none + disable-animations: true + script: bash ${{ github.workspace }}/frontend/scripts/android-E2E.sh + + - name: upload artifacts + if: always() + uses: actions/upload-artifact@v4 + with: + name: android-artifacts-${{matrix.runs-on}} + path: frontend/artifacts/ diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 64c8c7c8..5f9969a6 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -6,33 +6,33 @@ on: pull_request: branches: [master] +env: + LIST_REPO_E2E_TEST: ('lexicon','lexicon-kodefox') # list of repo name which want run e2e test + jobs: - build: + # get-env job is run to get env using output because we cannot use env context outside steps if: env.LIST_REPO_E2E_TEST + # https://github.com/actions/runner/issues/2372#issuecomment-1518528105 + + get-env: runs-on: ubuntu-latest + outputs: + LIST_REPO: ${{ env.LIST_REPO_E2E_TEST }} steps: - - uses: actions/checkout@v3 - - - uses: actions/setup-node@v3 - with: - node-version: '16.x' - - - name: Get yarn cache directory path - id: yarn-cache-dir-path - run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT + - run: echo "null" - - uses: actions/cache@v3 - id: yarn-cache - with: - path: ${{ steps.yarn-cache-dir-path.outputs.dir }} - key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} - restore-keys: | - ${{ runner.os }}-yarn- + path-filters: + needs: [get-env] + if: contains(needs.get-env.outputs.LIST_REPO,github.event.repository.name) + uses: ./.github/workflows/path-filters.yml - - name: Install dependencies - run: yarn + test: + uses: ./.github/workflows/test.yml - - name: Generate files - run: yarn generate + # Disable Detox tests because they require a connection with a Build Jet account. + # To enable these tests, ensure your repository is connected to Build Jet: https://buildjet.com/for-github-actions - - name: Run tests - run: yarn test + # e2e-test: + # needs: [path-filters, test] + # if: needs.path-filters.outputs.frontendSrc-changes == 'true' && contains(needs.get-env.outputs.LIST_REPO,github.event.repository.name) + # uses: ./.github/workflows/e2e-test-android.yml + # secrets: inherit diff --git a/.github/workflows/path-filters.yml b/.github/workflows/path-filters.yml new file mode 100644 index 00000000..6f868474 --- /dev/null +++ b/.github/workflows/path-filters.yml @@ -0,0 +1,54 @@ +name: Path Filtering + +on: + workflow_call: + outputs: + frontend-changes: + description: 'this is lexicon app changes status' + value: ${{ jobs.path-filters.outputs.frontend-changes }} + frontendSrc-changes: + description: 'this is lexicon app src only status' + value: ${{ jobs.path-filters.outputs.frontendSrc-changes }} + backend-changes: + description: 'this is Backend Prose changes status' + value: ${{ jobs.path-filters.outputs.backend-changes }} + +jobs: + path-filters: + runs-on: ubuntu-latest + outputs: + frontend-changes: ${{ steps.filter-frontend.outputs.isUpdated }} + frontendSrc-changes: ${{ steps.filter-frontendSrc.outputs.isUpdated }} + backend-changes: ${{ steps.filter-backend.outputs.isUpdated }} + + steps: + - uses: actions/checkout@v4 + + - uses: dorny/paths-filter@v3 + id: filter + with: + filters: | + backend: + - 'api/**' + frontend: + - 'frontend/**' + frontendSrc: + - 'frontend/src/**' + + # run only if 'backend' files were changed + - name: Output filter Backend Prose + id: filter-backend + if: steps.filter.outputs.backend == 'true' + run: echo "isUpdated=true" >> $GITHUB_OUTPUT + + # run only if 'frontend' files were changed + - name: Output filter Lexicon + id: filter-frontend + if: steps.filter.outputs.frontend == 'true' + run: echo "isUpdated=true" >> $GITHUB_OUTPUT + + # run only if 'frontend scene and component' files were changed + - name: Output filter Lexicon src + id: filter-frontendSrc + if: steps.filter.outputs.frontendSrc == 'true' + run: echo "isUpdated=true" >> $GITHUB_OUTPUT diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 00000000..bd85975f --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,34 @@ +name: test + +on: workflow_call + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: '20.x' + + - name: Get yarn cache directory path + id: yarn-cache-dir-path + run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT + + - uses: actions/cache@v4 + id: yarn-cache + with: + path: ${{ steps.yarn-cache-dir-path.outputs.dir }} + key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} + restore-keys: | + ${{ runner.os }}-yarn- + + - name: Install dependencies + run: yarn + + - name: Generate files + run: yarn generate + + - name: Run tests + run: yarn test diff --git a/api/deploy/Dockerfile b/api/deploy/Dockerfile index beef3819..d004daa1 100644 --- a/api/deploy/Dockerfile +++ b/api/deploy/Dockerfile @@ -1,4 +1,4 @@ -FROM keymetrics/pm2:16-alpine +FROM keymetrics/pm2:18-alpine WORKDIR /app diff --git a/api/package.json b/api/package.json index d51a9ae6..9fddd544 100644 --- a/api/package.json +++ b/api/package.json @@ -29,6 +29,7 @@ "nexus": "^1.4.0-next.11", "querystring": "^0.2.0", "set-cookie-parser": "^2.5.1", + "sharp": "^0.32.6", "snakecase-keys": "^3.2.0", "tough-cookie": "^4.1.3", "winston": "^3.10.0", diff --git a/api/src/__tests__/cookiesStringify.ts b/api/src/__tests__/cookiesStringify.ts deleted file mode 100644 index c08ee22d..00000000 --- a/api/src/__tests__/cookiesStringify.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { cookiesStringify } from '../helpers'; - -it('Stringify array of cookies to string', () => { - const inputCoookies = [ - '_t=token; path=/; expires=Sun, 08 Nov 2020 06:16:42 GMT; HttpOnly; SameSite=Lax', - '_forum_session=session; path=/; HttpOnly; SameSite=Lax', - ]; - const expectedOutput = - '_t=token; path=/; expires=Sun, 08 Nov 2020 06:16:42 GMT; HttpOnly; SameSite=Lax;_forum_session=session; path=/; HttpOnly; SameSite=Lax;'; - let stringCookies = cookiesStringify(inputCoookies); - expect(stringCookies).toEqual(expectedOutput); -}); diff --git a/api/src/client.ts b/api/src/client.ts index b9f9e546..0d2d08e8 100644 --- a/api/src/client.ts +++ b/api/src/client.ts @@ -3,14 +3,13 @@ import { ServerResponse } from 'http'; import axios, { AxiosResponse } from 'axios'; import axiosCookieJarSupport from 'axios-cookiejar-support'; import { CookieJar } from 'tough-cookie'; -import setCookie from 'set-cookie-parser'; import { CUSTOM_HEADER_TOKEN, PROSE_DISCOURSE_HOST } from './constants'; import { - cookiesStringify, generateToken, getCsrfSession, getModifiedUserAgent, + mergeCookies, } from './helpers'; export const discourseClient = axios.create({ @@ -62,23 +61,19 @@ export async function getClient(params: GetClientParams) { throw new Error('Not found or private.'); } - let cookies = response.headers['set-cookie']; + let newCookies = response.headers['set-cookie']; + + let resultMerge = mergeCookies({ oldCookies: cookies, newCookies }); /** - * This condition is used to check if there is a valid cookie. - * For the cookie to be refreshed, it must contain an _t cookie and - * it ensures that the cookie format is correct, excluding cookies from the login API, + * This condition checks if there is a valid cookie. + * To refresh the cookie, it must contain an `_t` cookie in the old cookies, indicating that the user is already logged in. + * It also checks for new cookies and ensures the cookie format is correct, excluding cookies from the login API, * which uses the `session.json` endpoint. */ - if ( - cookies && - // eslint-disable-next-line no-underscore-dangle - setCookie.parse(cookies, { map: true })._t && - !response.request.path.includes('session.json') - ) { - let stringCookie = cookiesStringify(cookies); - let token = generateToken(stringCookie); + if (resultMerge && !response.request.path.includes('session.json')) { + let token = generateToken(resultMerge); if (!context.response.headersSent) { context.response.setHeader(CUSTOM_HEADER_TOKEN, token); diff --git a/api/src/constants/server.ts b/api/src/constants/server.ts index e84b3600..ba5be7fc 100644 --- a/api/src/constants/server.ts +++ b/api/src/constants/server.ts @@ -5,7 +5,7 @@ config(); const ACCEPTED_LANGUAGE = 'en-US'; const CONTENT_FORM_URLENCODED = 'application/x-www-form-urlencoded'; const CONTENT_JSON = 'application/json'; -const CUSTOM_HEADER_TOKEN = 'X-Prose-Latest-Token'; +const CUSTOM_HEADER_TOKEN = 'x-prose-latest-token'; export { ACCEPTED_LANGUAGE, diff --git a/api/src/helpers/__tests__/cookiesStringify.test.ts b/api/src/helpers/__tests__/cookiesStringify.test.ts new file mode 100644 index 00000000..6f2444be --- /dev/null +++ b/api/src/helpers/__tests__/cookiesStringify.test.ts @@ -0,0 +1,60 @@ +import { cookiesStringify, mergeCookies } from '..'; + +const inputCookies = [ + '_t=token; path=/; expires=Sun, 08 Nov 2020 06:16:42 GMT; HttpOnly; SameSite=Lax', + '_forum_session=session; path=/; HttpOnly; SameSite=Lax', +]; +describe('cookiesStringify', () => { + it('Stringify array of cookies to string', () => { + const expectedOutput = '_t=token;_forum_session=session;'; + let stringCookies = cookiesStringify(inputCookies); + expect(stringCookies).toEqual(expectedOutput); + }); + it('Stringify string cookie to string', () => { + const expectedOutput = '_t=token;'; + let stringCookies = cookiesStringify(inputCookies[0]); + expect(stringCookies).toEqual(expectedOutput); + }); +}); + +describe('mergeCookies', () => { + it('should return empty string if there are no cookies or _t cookies at old cookies', () => { + const oldCookies = ''; + const oldCookiesWithoutT = + '_forum_session=oldSessionValue;_other_value=randomValue'; + + const result = mergeCookies({ oldCookies, newCookies: inputCookies }); + const result2 = mergeCookies({ + oldCookies: oldCookiesWithoutT, + newCookies: inputCookies, + }); + const result3 = mergeCookies({}); + const result4 = mergeCookies({ oldCookies }); + + const expectedOutput = ''; + + expect(result).toBe(expectedOutput); + expect(result2).toBe(expectedOutput); + expect(result3).toBe(expectedOutput); + expect(result4).toBe(expectedOutput); + }); + it('should replace all value of old cookie', () => { + const oldCookies = '_t=oldTokenValue;_forum_session=oldSessionValue;'; + + let result = mergeCookies({ oldCookies, newCookies: inputCookies }); + const expectedOutput = '_t=token;_forum_session=session;'; + + expect(result).toBe(expectedOutput); + }); + it('should replace old cookie with only provided value', () => { + const oldCookies = '_t=oldTokenValue;_forum_session=oldSessionValue;'; + const newCookies = [ + '_forum_session=newSession; path=/; HttpOnly; SameSite=Lax', + '_other_cookie=otherCookieValue; path=/; HttpOnly;', + ]; + let result = mergeCookies({ oldCookies, newCookies }); + const expectedOutput = + '_t=oldTokenValue;_forum_session=newSession;_other_cookie=otherCookieValue;'; + expect(result).toBe(expectedOutput); + }); +}); diff --git a/api/src/__tests__/decodeEncodeToken.ts b/api/src/helpers/__tests__/decodeEncodeToken.test.ts similarity index 85% rename from api/src/__tests__/decodeEncodeToken.ts rename to api/src/helpers/__tests__/decodeEncodeToken.test.ts index 4a5f758a..000a8bfb 100644 --- a/api/src/__tests__/decodeEncodeToken.ts +++ b/api/src/helpers/__tests__/decodeEncodeToken.test.ts @@ -1,4 +1,4 @@ -import { decodeToken, generateToken } from '../helpers'; +import { decodeToken, generateToken } from '..'; it('The input and output should same', () => { const inputCoookies = diff --git a/api/src/helpers/__tests__/errorHandler.test.ts b/api/src/helpers/__tests__/errorHandler.test.ts new file mode 100644 index 00000000..4e21f9ee --- /dev/null +++ b/api/src/helpers/__tests__/errorHandler.test.ts @@ -0,0 +1,220 @@ +import { + InvalidAccessError, + AuthorizationError, + errorHandler, + SessionExpiredError, +} from '..'; +import { + ChangeUsernameError, + EditPostError, + errorTypes, +} from '../../constants'; + +const baseMockError = { + name: '', + message: '', + isAxiosError: true, + config: {}, + response: { + status: 500, + statusText: '', + headers: {}, + data: {}, + config: { + headers: { + Cookie: '', + }, + }, + }, + toJSON: function () { + throw new Error('custom json function'); + }, +}; + +describe('errorHandler', () => { + it('should throw an error for username already taken', () => { + const mockAxiosErrorUserName = { + ...baseMockError, + name: 'AxiosError', + message: 'Request failed with status code 500', + response: { + ...baseMockError.response, + data: { + errors: ['This username is already taken'], + }, + }, + }; + + expect(() => errorHandler(mockAxiosErrorUserName)).toThrowError( + 'This username is already taken', + ); + }); + + it('should throw an error for failed request', () => { + const mockOtherError = { + ...baseMockError, + response: { + ...baseMockError.response, + data: { + failed: 'Error message for failed request', + }, + }, + }; + + expect(() => errorHandler(mockOtherError)).toThrowError( + 'Error message for failed request', + ); + }); + + it('should throw an error for edit post error', () => { + const mockAxiosErrorEditPostError = { + ...baseMockError, + response: { + ...baseMockError.response, + data: { + errors: [EditPostError], + }, + }, + }; + + expect(() => errorHandler(mockAxiosErrorEditPostError)).toThrowError( + `You've passed the time limit to edit this post.`, + ); + }); + + it('should throw an error for change username error', () => { + const mockAxiosErrorChangeUsernameError = { + ...baseMockError, + response: { + ...baseMockError.response, + data: { + errors: [ChangeUsernameError], + }, + }, + }; + + expect(() => errorHandler(mockAxiosErrorChangeUsernameError)).toThrowError( + 'This username is already taken', + ); + }); + + it('should throw an error invalid access', () => { + const mockAxiosErrorInvalidAccessError = { + ...baseMockError, + response: { + ...baseMockError.response, + data: { + errors: [], + error_type: errorTypes.invalidAccess, + }, + }, + }; + + expect(() => errorHandler(mockAxiosErrorInvalidAccessError)).toThrowError( + new InvalidAccessError(), + ); + }); + + it('should throw an error unauthenticated access', () => { + const mockAxiosErrorUnauthenticatedAccessError = { + ...baseMockError, + response: { + ...baseMockError.response, + data: { + errors: [], + error_type: errorTypes.unauthenticatedAccess, + }, + }, + }; + + expect(() => + errorHandler(mockAxiosErrorUnauthenticatedAccessError), + ).toThrowError(new AuthorizationError()); + }); + + it('should throw an error session expire', () => { + const mockAxiosErrorSessionExpire = { + ...baseMockError, + response: { + ...baseMockError.response, + config: { + headers: { + Cookie: + '_t=jUpkKRhp1mKWuBp0IxBqUT0uYem7mAeruq4iqIWxySvYtQw26czsuhT7YwB7stg4', + }, + }, + data: { + errors: [], + error_type: errorTypes.unauthenticatedAccess, + }, + }, + }; + + expect(() => errorHandler(mockAxiosErrorSessionExpire)).toThrowError( + new SessionExpiredError(), + ); + }); + + it('should throw an exceeds file size error', () => { + const mockAxiosErrorFileSize = { + ...baseMockError, + response: { + ...baseMockError.response, + status: 413, + config: { + headers: { + Cookie: + '_t=jUpkKRhp1mKWuBp0IxBqUT0uYem7mAeruq4iqIWxySvYtQw26czsuhT7YwB7stg4', + }, + }, + data: { + error: 'File size exceeds max size', + }, + }, + }; + + expect(() => errorHandler(mockAxiosErrorFileSize)).toThrowError( + 'The file size of your image exceeds the maximum allowed file size.', + ); + }); + + it('should throw an forbidden access', () => { + const mockAxiosErrorForbiddenAccess = { + ...baseMockError, + response: { + ...baseMockError.response, + status: 403, + data: 'Forbidden Access Error', + }, + }; + + expect(() => errorHandler(mockAxiosErrorForbiddenAccess)).toThrowError( + 'Forbidden Access Error', + ); + }); + + it('should throw an error for private topic error', () => { + const mockAxiosErrorPrivateTopic = { + ...baseMockError, + response: { + ...baseMockError.response, + data: { + errors: ['The topic cannot be accessed as it is a private topic.'], + }, + }, + }; + + expect(() => errorHandler(mockAxiosErrorPrivateTopic)).toThrowError( + 'The topic cannot be accessed as it is a private topic.', + ); + }); + + it('should throw an error beside axios error', () => { + const mockError: Error = { + name: 'custom error', + message: 'throw custom error', + }; + + expect(() => errorHandler(mockError)).toThrowError('throw custom error'); + }); +}); diff --git a/api/src/helpers/__tests__/getModifiedUserAgent.test.ts b/api/src/helpers/__tests__/getModifiedUserAgent.test.ts new file mode 100644 index 00000000..8c4a8382 --- /dev/null +++ b/api/src/helpers/__tests__/getModifiedUserAgent.test.ts @@ -0,0 +1,13 @@ +import { getModifiedUserAgent } from '..'; + +it('Should check mobile and not mobile agent', () => { + const notMobileAgent = + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) AltairGraphQLClient/6.3.1 Chrome/116.0.5845.190 Electron/26.2.2 Safari/537.36'; + let mobileAgent = + 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148'; + + expect(getModifiedUserAgent(notMobileAgent)).toEqual(`${notMobileAgent} `); + expect(getModifiedUserAgent(mobileAgent)).toEqual( + `${mobileAgent} DiscourseHub`, + ); +}); diff --git a/api/src/__tests__/getPosterTypeDetails.ts b/api/src/helpers/__tests__/getPosterTypeDetails.test.ts similarity index 98% rename from api/src/__tests__/getPosterTypeDetails.ts rename to api/src/helpers/__tests__/getPosterTypeDetails.test.ts index b391bb45..50a12134 100644 --- a/api/src/__tests__/getPosterTypeDetails.ts +++ b/api/src/helpers/__tests__/getPosterTypeDetails.test.ts @@ -1,4 +1,4 @@ -import { getPosterTypeDetails } from '../helpers/getPosterTypeDetails'; +import { getPosterTypeDetails } from '../getPosterTypeDetails'; describe('getPosterTypeDetails', () => { describe('English', () => { diff --git a/api/src/__tests__/getTopicAuthor.ts b/api/src/helpers/__tests__/getTopicAuthor.test.ts similarity index 95% rename from api/src/__tests__/getTopicAuthor.ts rename to api/src/helpers/__tests__/getTopicAuthor.test.ts index 3168d75c..5ebad5b1 100644 --- a/api/src/__tests__/getTopicAuthor.ts +++ b/api/src/helpers/__tests__/getTopicAuthor.test.ts @@ -1,8 +1,5 @@ -import { - getTopicAuthor, - getTopicAuthorUserId, -} from '../helpers/getTopicAuthor'; -import { PosterUnion } from '../types'; +import { getTopicAuthor, getTopicAuthorUserId } from '../getTopicAuthor'; +import { PosterUnion } from '../../types'; function getUserWithId(userId: number, description: string): PosterUnion { return { diff --git a/api/src/__tests__/getTopicPostPath.ts b/api/src/helpers/__tests__/getTopicPostPath.test.ts similarity index 88% rename from api/src/__tests__/getTopicPostPath.ts rename to api/src/helpers/__tests__/getTopicPostPath.test.ts index 214f8f0c..1ab2d527 100644 --- a/api/src/__tests__/getTopicPostPath.ts +++ b/api/src/helpers/__tests__/getTopicPostPath.test.ts @@ -1,4 +1,4 @@ -import { getTopicPostPath } from '../helpers'; +import { getTopicPostPath } from '..'; it('should return posts when the input is array of numbers', () => { expect(getTopicPostPath([2, 3, 4])).toEqual('/posts'); diff --git a/api/src/__tests__/getTopicTimings.ts b/api/src/helpers/__tests__/getTopicTimings.test.ts similarity index 89% rename from api/src/__tests__/getTopicTimings.ts rename to api/src/helpers/__tests__/getTopicTimings.test.ts index 2cb215eb..80f6106d 100644 --- a/api/src/__tests__/getTopicTimings.ts +++ b/api/src/helpers/__tests__/getTopicTimings.test.ts @@ -1,4 +1,4 @@ -import { getTopicTimings } from '../helpers'; +import { getTopicTimings } from '..'; it('Should send back timings input object', () => { const inputPost = [2, 3, 4]; diff --git a/api/src/__tests__/getUpdatedLikedTopic.ts b/api/src/helpers/__tests__/getUpdatedLikedTopic.test.ts similarity index 92% rename from api/src/__tests__/getUpdatedLikedTopic.ts rename to api/src/helpers/__tests__/getUpdatedLikedTopic.test.ts index df01eea1..4728c6d8 100644 --- a/api/src/__tests__/getUpdatedLikedTopic.ts +++ b/api/src/helpers/__tests__/getUpdatedLikedTopic.test.ts @@ -1,4 +1,4 @@ -import { getUpdatedLikedTopic } from '../helpers'; +import { getUpdatedLikedTopic } from '..'; const likeCount = 5; const currentLikedTopicResponse = { diff --git a/api/src/helpers/__tests__/likeErrorHandler.test.ts b/api/src/helpers/__tests__/likeErrorHandler.test.ts new file mode 100644 index 00000000..7fdc72d5 --- /dev/null +++ b/api/src/helpers/__tests__/likeErrorHandler.test.ts @@ -0,0 +1,97 @@ +import { AxiosError } from 'axios'; + +import { LikableEntity, likeErrorHandler } from '../likeErrorHandler'; + +const mockAxiosError: AxiosError = { + name: 'AxiosError', + message: 'Request failed with status code 403', + isAxiosError: true, + config: {}, + response: { + status: 403, + data: {}, + statusText: 'Forbidden', + headers: {}, + config: {}, + }, + toJSON: function () { + throw new Error('custom json function'); + }, +}; + +const actionsSummary = [ + { + id: 1, + hidden: false, + acted: false, + canUndo: false, + canAct: true, + count: 1, + }, + { + id: 2, + hidden: false, + acted: true, + canUndo: false, + canAct: true, + count: 1, + }, + { + id: 2, + hidden: false, + acted: false, + canUndo: false, + canAct: true, + count: 1, + }, +]; +const likableEntityPost: LikableEntity = 'post'; +const like = false; + +describe('likeErrorHandler', () => { + it('should throw error when like action summary is not provided for the post author', () => { + expect(() => + likeErrorHandler(mockAxiosError, { + actionsSummary: [actionsSummary[0]], + likableEntity: likableEntityPost, + like, + }), + ).toThrowError( + `You're not permitted to do like actions to your own ${likableEntityPost}.`, + ); + }); + + it(`should throw error You've liked this post when like again same post`, () => { + expect(() => + likeErrorHandler(mockAxiosError, { + actionsSummary, + likableEntity: likableEntityPost, + like: !like, + }), + ).toThrowError(`You've liked this ${likableEntityPost} before.`); + }); + + it(`should throw error when unlike post which already unlike`, () => { + expect(() => + likeErrorHandler(mockAxiosError, { + actionsSummary: [actionsSummary[2]], + likableEntity: likableEntityPost, + like: like, + }), + ).toThrowError( + `You can't unlike a ${likableEntityPost} you haven't liked before.`, + ); + }); + + it(`should throw error because pass limit unlike`, () => { + expect(() => + likeErrorHandler(mockAxiosError, { + actionsSummary: [actionsSummary[1]], + likableEntity: likableEntityPost, + like: like, + }), + ).toThrowError( + `You've passed the time limit to unlike this ${likableEntityPost}.`, + ); + }); +}); diff --git a/api/src/__tests__/parseTopicUrl.ts b/api/src/helpers/__tests__/parseTopicUrl.test.ts similarity index 97% rename from api/src/__tests__/parseTopicUrl.ts rename to api/src/helpers/__tests__/parseTopicUrl.test.ts index 0f7dbd88..f66fb6d2 100644 --- a/api/src/__tests__/parseTopicUrl.ts +++ b/api/src/helpers/__tests__/parseTopicUrl.test.ts @@ -1,4 +1,4 @@ -import { FilterInput, parseTopicUrl } from '../helpers'; +import { FilterInput, parseTopicUrl } from '..'; it('latest', () => { const filterInput: FilterInput = { diff --git a/api/src/helpers/__tests__/poll.test.ts b/api/src/helpers/__tests__/poll.test.ts new file mode 100644 index 00000000..423852ce --- /dev/null +++ b/api/src/helpers/__tests__/poll.test.ts @@ -0,0 +1,182 @@ +import { Poll, PollsVotes, PreloaderUnion } from '../../types'; +import { formatPolls, formatPollsVotes, formatPreloadedVoters } from '..'; + +const polls: Array = [ + { + name: 'Poll 1', + type: 'regular', + status: 'open', + public: true, + results: 'always', + options: [ + { id: '1', html: 'Option 1', votes: 0 }, + { id: '2', html: 'Option 2', votes: 1 }, + ], + voters: 1, + preloadedVoters: [ + { + id: 1, + username: 'user1', + name: 'User One', + avatarTemplate: 'avatar1', + title: null, + }, + ], + chartType: 'bar', + }, + { + name: 'Poll 2', + type: 'multiple', + status: 'closed', + public: false, + results: 'on_vote', + options: [ + { id: '1', html: 'Option A', votes: 0 }, + { id: '2', html: 'Option B', votes: 1 }, + { id: '3', html: 'Option C', votes: 0 }, + ], + voters: 1, + preloadedVoters: [ + { + id: 2, + username: 'user2', + name: 'User Two', + avatarTemplate: 'avatar2', + title: 'Title', + }, + ], + chartType: 'pie', + }, +]; + +describe('formatPreloadedVoters', () => { + test('should format preloadedVoters data correctly when input is an object', () => { + const preloadedVoters: PreloaderUnion = { + '1': [ + { + id: 1, + username: 'user1', + name: 'User One', + avatarTemplate: 'avatar1', + title: null, + }, + ], + '2': [ + { + id: 2, + username: 'user2', + name: 'User Two', + avatarTemplate: 'avatar2', + title: 'Title', + }, + ], + }; + + const expectedOutput = { + preloadedVoters: [ + { pollOptionId: '1', users: preloadedVoters['1'] }, + { pollOptionId: '2', users: preloadedVoters['2'] }, + ], + }; + + expect(formatPreloadedVoters(preloadedVoters)).toEqual(expectedOutput); + }); + + test('should format preloadedVoters data correctly when input is an array', () => { + const preloadedVoters: PreloaderUnion = [ + { + id: 1, + username: 'user1', + name: 'User One', + avatarTemplate: 'avatar1', + title: null, + }, + { + id: 2, + username: 'user2', + name: 'User Two', + avatarTemplate: 'avatar2', + title: 'Title', + }, + ]; + + const expectedOutput = { + preloadedVoters: [{ pollOptionId: '', users: preloadedVoters }], + }; + + expect(formatPreloadedVoters(preloadedVoters)).toEqual(expectedOutput); + }); +}); + +describe('formatPollsVotes', () => { + it('should return null when input is null and undefined', () => { + expect(formatPollsVotes(null)).toBeNull(); + expect(formatPollsVotes(undefined)).toBeNull(); + }); + + it('should return empty array when input is an empty object', () => { + expect(formatPollsVotes({})).toEqual([]); + }); + + it('should format pollsVotes data correctly', () => { + const pollsVotes = { + poll1: ['option1', 'option2'], + poll2: ['option3', 'option4'], + }; + const expectedFormattedPollsVotes = [ + { pollName: 'poll1', pollOptionIds: ['option1', 'option2'] }, + { pollName: 'poll2', pollOptionIds: ['option3', 'option4'] }, + ]; + expect(formatPollsVotes(pollsVotes)).toEqual(expectedFormattedPollsVotes); + }); +}); + +describe('formatPolls', () => { + test('should return null for formattedPolls and formattedPollsVotes if polls parameter is null or undefined', () => { + expect(formatPolls(null)).toEqual({ + formattedPolls: null, + formattedPollsVotes: null, + }); + expect(formatPolls(undefined)).toEqual({ + formattedPolls: null, + formattedPollsVotes: null, + }); + }); + + test('should format polls data correctly when pollsVotes parameter is null', () => { + const expectedFormattedPolls = polls.map((poll) => ({ + ...poll, + preloadedVoters: [{ pollOptionId: '', users: poll.preloadedVoters }], + })); + const expectedOutput = { + formattedPolls: expectedFormattedPolls, + formattedPollsVotes: null, + }; + + expect(formatPolls(polls)).toEqual(expectedOutput); + }); + + test('should format polls data correctly when pollsVotes parameter is provided', () => { + const pollsVotes: PollsVotes = { + 'Poll 1': ['1', '2'], + 'Poll 2': ['1', '2', '3'], + }; + + const expectedFormattedPolls = polls.map((poll) => ({ + ...poll, + preloadedVoters: [{ pollOptionId: '', users: poll.preloadedVoters }], + })); + const expectedOutput = { + formattedPolls: expectedFormattedPolls, + formattedPollsVotes: [ + { pollName: 'Poll 1', pollOptionIds: ['1', '2'] }, + { + pollName: 'Poll 2', + pollOptionIds: ['1', '2', '3'], + }, + ], + }; + + expect(formatPolls(polls, pollsVotes)).toEqual(expectedOutput); + }); +}); diff --git a/api/src/__tests__/privateMessagesMerger.ts b/api/src/helpers/__tests__/privateMessagesMerger.test.ts similarity index 93% rename from api/src/__tests__/privateMessagesMerger.ts rename to api/src/helpers/__tests__/privateMessagesMerger.test.ts index 3bab620d..0f3d6fc3 100644 --- a/api/src/__tests__/privateMessagesMerger.ts +++ b/api/src/helpers/__tests__/privateMessagesMerger.test.ts @@ -1,7 +1,6 @@ -import { privateMessagesMerger } from '../helpers'; -import { DiscoursePMInput, PMOutput } from '../types'; - -import { createMessage, createUser } from './data'; +import { privateMessagesMerger } from '..'; +import { DiscoursePMInput, PMOutput } from '../../types'; +import { createMessage, createUser } from '../../__tests__/data'; describe('privateMessagesMerger', () => { const topicListDefaults = { diff --git a/api/src/__tests__/processRawContent.ts b/api/src/helpers/__tests__/processRawContent.test.ts similarity index 83% rename from api/src/__tests__/processRawContent.ts rename to api/src/helpers/__tests__/processRawContent.test.ts index beebca59..a96da6cd 100644 --- a/api/src/__tests__/processRawContent.ts +++ b/api/src/helpers/__tests__/processRawContent.test.ts @@ -2,7 +2,8 @@ import { generateMarkdownContent, getCompleteImageVideoUrls, getEmojiImageUrls, -} from '../helpers'; + userActivityMarkdownContent, +} from '..'; describe('getCompleteImageUrls return image urls from html tags', () => { it('should return the last url from srcset in img tag if any', () => { @@ -186,3 +187,50 @@ describe('generate emoji url from image tag', () => { expect(getEmojiImageUrls(content3)).toEqual([]); }); }); + +describe('generate new content for user activity', () => { + it('it should return Content based input', () => { + const content = 'Hello\n who is this'; + const content1 = 'Just want to test\n\n something'; + + expect(userActivityMarkdownContent(content)).toEqual(content); + expect(userActivityMarkdownContent(content1)).toEqual(content1); + }); + it('it should replace content image', () => { + const contentImage = + 'Hello\n [exampleImage1]'; + const contentImageSrc = + 'download'; + + expect(userActivityMarkdownContent(contentImage)).toEqual( + 'Hello\n ![exampleImage1](https://image.jpeg)', + ); + expect(userActivityMarkdownContent(contentImageSrc)).toEqual( + '![undefined](https://wiki.kfox.io/uploads/default/original.jpeg)', + ); + }); + it('it should convert emoji', () => { + const emojiContent = + 'Hello\n :heart:'; + + expect(userActivityMarkdownContent(emojiContent)).toEqual( + 'Hello\n ![emoji-:heart:](https://image/heart.png?v=12)', + ); + }); + it('it should convert mention', () => { + const mentionContent = + 'Is this true? @marcello'; + + expect(userActivityMarkdownContent(mentionContent)).toEqual( + 'Is this true? @marcello', + ); + }); + it('it should convert Link', () => { + const mentionContent = + 'Hello'; + + expect(userActivityMarkdownContent(mentionContent)).toEqual( + '[Hello](https://www.google.com)', + ); + }); +}); diff --git a/api/src/helpers/__tests__/topicDetail.test.ts b/api/src/helpers/__tests__/topicDetail.test.ts new file mode 100644 index 00000000..28ee88c6 --- /dev/null +++ b/api/src/helpers/__tests__/topicDetail.test.ts @@ -0,0 +1,52 @@ +import { getTopicPostPath, validateTopicDetailOptionalArgs } from '..'; + +describe('getTopicPostPath', () => { + it('should return an empty string when input is undefined', () => { + expect(getTopicPostPath()).toBe(''); + }); + + it('should return a path with a single post number', () => { + const postNumber = 123; + expect(getTopicPostPath(postNumber)).toBe('/123'); + }); + + it('should return a path for multiple posts when input is an array', () => { + const postIds = [456, 789]; + expect(getTopicPostPath(postIds)).toBe('/posts'); + }); + + it('should return an empty string when input is undefined', () => { + expect(getTopicPostPath(undefined)).toBe(''); + }); +}); + +describe('validateTopicDetailOptionalArgs', () => { + it('should return an error to only provide post id or number', () => { + const args = { + postIds: [1], + postNumber: 1, + }; + expect(() => validateTopicDetailOptionalArgs(args)).toThrowError( + 'Please provide either only the post IDs or the post number', + ); + }); + + it('should return an error to only provide includeFirstPost', () => { + const args = { + postIds: [1], + includeFirstPost: true, + }; + expect(() => validateTopicDetailOptionalArgs(args)).toThrowError( + 'The first post cannot be included when post IDs are provided', + ); + }); + + it('should not return error', () => { + const args = {}; + const args1 = { postIds: [1, 2] }; + const args2 = { postNumber: 123 }; + expect(() => validateTopicDetailOptionalArgs(args)).not.toThrow(); + expect(() => validateTopicDetailOptionalArgs(args1)).not.toThrow(); + expect(() => validateTopicDetailOptionalArgs(args2)).not.toThrow(); + }); +}); diff --git a/api/src/helpers/auth.ts b/api/src/helpers/auth.ts index a266a5a5..50e9cc35 100644 --- a/api/src/helpers/auth.ts +++ b/api/src/helpers/auth.ts @@ -5,10 +5,15 @@ import camelcaseKey from 'camelcase-keys'; import snakecaseKeys from 'snakecase-keys'; import { discourseClient } from '../client'; -import { CONTENT_FORM_URLENCODED } from '../constants'; +import { + ACCEPTED_LANGUAGE, + CONTENT_FORM_URLENCODED, + CONTENT_JSON, +} from '../constants'; import { cookiesStringify } from './cookiesStringify'; import { SessionExpiredError } from './customErrors'; +import { getSiteDataLexiconPlugin } from './siteSettings'; async function getCsrfSession(cookies?: string) { let { @@ -30,7 +35,23 @@ type Credentials = { secondFactorToken?: string | null; }; type CsrfSession = { csrf: string; initialSessionCookie: string }; +type HpSession = { + initialSessionCookie: string; + passwordConfirmation: string; + challenge: string; +}; type AuthRequest = Credentials & CsrfSession & { client: AxiosInstance }; +type AppleAuthRequest = { identityToken: string } & CsrfSession & { + client: AxiosInstance; + }; +type LoginLinkRequest = { emailToken: string } & CsrfSession & { + client: AxiosInstance; + }; + +type ActivateAccountRequest = { emailToken: string } & CsrfSession & + HpSession & { + client: AxiosInstance; + }; function generateToken(cookies: string) { const buffer = Buffer.from(cookies); @@ -96,9 +117,16 @@ async function authenticate(authRequest: AuthRequest) { } let stringCookie = cookiesStringify(headers['set-cookie']); let token = generateToken(stringCookie); + + let siteData = await getSiteDataLexiconPlugin({ + client, + cookies: headers['set-cookie'], + }); + return { ...camelcaseKey(data, { deep: true }), token, + enableLexiconPushNotifications: siteData.enableLexiconPushNotifications, }; } @@ -119,7 +147,11 @@ async function getHpChallenge(csrfSession: CsrfSession) { await discourseClient.get('/users/hp.json', config); let { errors: oldVersionErrors, error_type: oldVersionErrorType } = oldVersionData; - if (oldVersionErrors && oldVersionErrorType === 'not_found') { + if ( + oldVersionErrors && + (oldVersionErrorType === 'not_found' || + oldVersionErrorType === 'not_logged_in') + ) { let { data: newVersionData, headers: newVersionHeaders } = await discourseClient.get('/session/hp.json', config); data = newVersionData; @@ -170,6 +202,145 @@ async function checkSession(authClient: AxiosInstance) { } } +async function authenticateApple(appleAuthRequest: AppleAuthRequest) { + let { csrf, initialSessionCookie, identityToken, client } = appleAuthRequest; + + let config = { + headers: { + 'Accept-Language': ACCEPTED_LANGUAGE, + 'Content-Type': CONTENT_JSON, + 'x-csrf-token': csrf, + }, + withCredentials: true, + Cookie: initialSessionCookie, + }; + + let { data, headers } = await client.post( + '/lexicon/auth/apple/login.json', + { + id_token: identityToken, + }, + config, + ); + let { error } = data; + if (error) { + throw new Error(error); + } + let stringCookie = cookiesStringify(headers['set-cookie']); + let token = generateToken(stringCookie); + + let siteData = await getSiteDataLexiconPlugin({ + client, + cookies: headers['set-cookie'], + }); + + return { + ...camelcaseKey(data, { deep: true }), + token, + enableLexiconPushNotifications: siteData.enableLexiconPushNotifications, + }; +} + +async function authenticateActivateAccount( + activateAccountRequest: ActivateAccountRequest, +) { + let { csrf, initialSessionCookie, emailToken, client, ...hpValue } = + activateAccountRequest; + + let config = { + headers: { + 'Accept-Language': ACCEPTED_LANGUAGE, + 'Content-Type': CONTENT_FORM_URLENCODED, + 'x-csrf-token': csrf, + }, + withCredentials: true, + Cookie: initialSessionCookie, + }; + + let snakecaseBody = snakecaseKeys({ + token: emailToken, + ...hpValue, + }); + + let { data, headers } = await client.post( + `/lexicon/auth/activate_account.json`, + stringify(snakecaseBody), + config, + ); + let { error } = data; + if (error) { + throw new Error(error); + } + let stringCookie = cookiesStringify(headers['set-cookie']); + let token = generateToken(stringCookie); + + let siteData = await getSiteDataLexiconPlugin({ + client, + cookies: headers['set-cookie'], + }); + + return { + ...camelcaseKey(data, { deep: true }), + token, + enableLexiconPushNotifications: siteData.enableLexiconPushNotifications, + }; +} + +async function authenticateLoginLink(authRequest: LoginLinkRequest) { + let { csrf, initialSessionCookie, emailToken, client } = authRequest; + + let config = { + headers: { + 'x-csrf-token': csrf, + 'Content-Type': CONTENT_FORM_URLENCODED, + }, + withCredentials: true, + Cookie: initialSessionCookie, + }; + let body = stringify({ token: emailToken }); + + let { data, headers } = await client.post( + `/session/email-login/${emailToken}.json`, + body, + config, + ); + let { error, failed } = data; + if (failed) { + return { + ...camelcaseKey(data, { deep: true }), + }; + } + if (error) { + throw new Error(error); + } + let stringCookie = cookiesStringify(headers['set-cookie']); + let token = generateToken(stringCookie); + + let configUser = { + headers: { + 'x-csrf-token': csrf, + }, + withCredentials: true, + Cookie: headers['set-cookie'], + }; + let { data: userData } = await client.get( + `/lexicon/auth/user.json`, + configUser, + ); + + let siteUrl = `/site.json`; + let { + data: { lexicon }, + } = await client.get(siteUrl, configUser); + + return { + ...camelcaseKey(userData, { deep: true }), + token, + enableLexiconPushNotifications: + lexicon?.settings.lexicon_push_notifications_enabled || false, + }; +} + export { getCsrfSession, authenticate, @@ -177,4 +348,7 @@ export { generateToken, getHpChallenge, checkSession, + authenticateApple, + authenticateActivateAccount, + authenticateLoginLink, }; diff --git a/api/src/helpers/cookiesStringify.ts b/api/src/helpers/cookiesStringify.ts index d6d8cc7f..07ea1f16 100644 --- a/api/src/helpers/cookiesStringify.ts +++ b/api/src/helpers/cookiesStringify.ts @@ -1,10 +1,27 @@ import setCookie from 'set-cookie-parser'; -export function cookiesStringify(cookies: Array) { +/** + * This function converts a cookie into the "name=value;" format. + * It is used based on header cookies at the Discourse website, + * where it only shows the cookie's name and value without other data. + * + * @param cookie - The string of the cookie. + * @returns A string representing the cookie in the new format to be used at Lexicon. + */ + +function joinCookieString(cookie: string): string { + let cookies = setCookie.parse(cookie, { decodeValues: false })[0]; + + let newCookie = cookies.name + '=' + cookies.value + ';'; + + return newCookie; +} + +export function cookiesStringify(cookies: Array | string) { let cookieString = ''; if (Array.isArray(cookies)) { for (let cookie of cookies) { - cookieString = cookieString + joinCookieString(cookie) + ';'; + cookieString += joinCookieString(cookie); } } if (typeof cookies === 'string') { @@ -13,6 +30,54 @@ export function cookiesStringify(cookies: Array) { return cookieString; } -function joinCookieString(cookie: string): string { - return setCookie.splitCookiesString(cookie).join(';'); +/** + * This function is used to replace and add new cookies when there are newCookies. + * + * When there is no value of _t in oldCookies or newCookies is undefined, it will return an empty string. + * + * It parses the oldCookies and newCookies, merges the new cookies into the old ones, + * and returns the resulting merged cookie string. + * + * @param {Object} param The parameters object contains: + * - {string|undefined} oldCookies: The old cookies string + * - {Array|undefined} newCookies: list of cookies which can get from response[set-cookies] + * + * @returns {string} The merged cookies string or an empty string if conditions are not met. + */ +export function mergeCookies({ + oldCookies, + newCookies, +}: { + oldCookies?: string; + newCookies?: Array; +}) { + // example old cookies format '_t=value;_forum=session;' + let parseOldCookie = oldCookies + ? setCookie.parse(oldCookies.split(';'), { + map: true, + decodeValues: false, + }) + : {}; + + // eslint-disable-next-line no-underscore-dangle + if (!oldCookies || !parseOldCookie._t || !newCookies) { + return ''; + } + const parsedNewCookies = setCookie.parse(newCookies, { + decodeValues: false, + }); + + // Replaces the old cookie value with the new cookie value if the same cookie name exists. + // Creates a new cookie entry if the new cookie name is not found in the old cookies. + + parsedNewCookies.forEach((newCookie) => { + parseOldCookie[newCookie.name] = { + name: newCookie.name, + value: newCookie.value, + }; + }); + + return Object.entries(parseOldCookie) + .map(([key, { value }]) => `${key}=${value};`) + .join(''); } diff --git a/api/src/helpers/getTopicAuthor.ts b/api/src/helpers/getTopicAuthor.ts index ddaa5298..28c19227 100644 --- a/api/src/helpers/getTopicAuthor.ts +++ b/api/src/helpers/getTopicAuthor.ts @@ -1,10 +1,14 @@ -import { PosterUnion } from '../types'; +import { PosterUnion, TopicPoster } from '../types'; import { getPosterTypeDetails } from './getPosterTypeDetails'; +/** + * Deprecated type TopicPoster which will be remove in version 3 + */ + export function getTopicAuthor( - posters: Readonly>, -): PosterUnion | undefined { + posters: Readonly>, +): PosterUnion | TopicPoster | undefined { return posters.find((poster) => { const { isAuthor } = getPosterTypeDetails(poster.description); return isAuthor; @@ -12,15 +16,15 @@ export function getTopicAuthor( } export function getTopicAuthorUserId( - posters: Readonly>, + posters: Readonly>, ): number | undefined { const author = getTopicAuthor(posters); if (author) { if ('userId' in author) { - return author.userId; + return author.userId || undefined; } else if ('user' in author) { - return author.user.id; + return author.user?.id; } } } diff --git a/api/src/helpers/index.ts b/api/src/helpers/index.ts index 24ef6acb..6576e261 100644 --- a/api/src/helpers/index.ts +++ b/api/src/helpers/index.ts @@ -12,3 +12,4 @@ export * from './privateMessagesMerger'; export * from './processRawContent'; export * from './topicDetail'; export * from './poll'; +export * from './siteSettings'; diff --git a/api/src/helpers/likeErroHandler.ts b/api/src/helpers/likeErrorHandler.ts similarity index 100% rename from api/src/helpers/likeErroHandler.ts rename to api/src/helpers/likeErrorHandler.ts diff --git a/api/src/helpers/processRawContent.ts b/api/src/helpers/processRawContent.ts index 0eff6be5..e012822a 100644 --- a/api/src/helpers/processRawContent.ts +++ b/api/src/helpers/processRawContent.ts @@ -16,6 +16,9 @@ const emojiBBCodeRegex = /(?<=^|\s):\w+:(?:t\d+:)?/g; const emojiImageTagRegex = //g; const emojiTitleRegex = /title="([^"]+)"/g; +const userActivityContentRegex = + /(?:]*src(?:set)?="(.+?)"(?:[^>]*title="([^"]*)")?(?:[^>]*class="([^"]*)")?[^>]*>)|(?:]* href="((https?:)?\/\/[^ ]*\.(?:jpe?g|png|gif|heic|heif|mov|mp4|webm|avi|wmv|flv|webp))"([^>]*?)title="([^"]*)"\s*>(\[.*?\])?<\/a>)|(?:]* class="mention" href="\/u\/([^"]+)">@(.*?)<\/a>)|(?:]* href="([^"]+)"[^>]*>(.*?)<\/a>)/g; + function handleRegexResult( result: RegExpMatchArray, host: string, @@ -183,3 +186,43 @@ export function getMention( return handleRegexResult(result, host, mentionRegex); } } + +export function userActivityMarkdownContent(content: string) { + const markdown = content.replace( + userActivityContentRegex, + ( + _, + imgSrc: string, + imgTitle: string, + imgClass: string, + aHref: string, + _https, + _dataHref, + aTitle: string, + _emptyMention, + _urlName, + nameMention, + linkHref, + linkText, + ) => { + let modifiedImageMarkdown = ``; + + if (imgSrc) { + modifiedImageMarkdown = `![${ + imgClass === 'emoji' || imgClass === 'emoji only-emoji' + ? 'emoji-' + : '' + }${imgTitle}](${imgSrc})`; + } else if (aHref) { + modifiedImageMarkdown = `![${aTitle}](${aHref})`; + } else if (nameMention) { + modifiedImageMarkdown = `@${nameMention}`; + } else if (linkHref && linkText) { + modifiedImageMarkdown = `[${linkText}](${linkHref})`; + } + + return modifiedImageMarkdown; + }, + ); + return markdown; +} diff --git a/api/src/helpers/siteSettings.ts b/api/src/helpers/siteSettings.ts new file mode 100644 index 00000000..4470f561 --- /dev/null +++ b/api/src/helpers/siteSettings.ts @@ -0,0 +1,24 @@ +import { AxiosInstance } from 'axios'; + +type GetSiteDataLexiconParams = { + client: AxiosInstance; + cookies: string; +}; +export async function getSiteDataLexiconPlugin( + params: GetSiteDataLexiconParams, +) { + const { client, cookies } = params; + let config = { + withCredentials: true, + Cookie: cookies, + }; + + let siteUrl = `/site.json`; + + let { data } = await client.get(siteUrl, config); + + return { + enableLexiconPushNotifications: + data?.lexicon?.settings.lexicon_push_notifications_enabled || false, + }; +} diff --git a/api/src/resolvers/auth/activateAccountMutation.ts b/api/src/resolvers/auth/activateAccountMutation.ts new file mode 100644 index 00000000..4563904f --- /dev/null +++ b/api/src/resolvers/auth/activateAccountMutation.ts @@ -0,0 +1,37 @@ +import { FieldResolver, mutationField, stringArg } from 'nexus'; + +import { + authenticateActivateAccount, + getCsrfSession, + getHpChallenge, +} from '../../helpers'; +import { Context } from '../../types'; + +export let activateAccountMutationResolver: FieldResolver< + 'Mutation', + 'activateAccount' +> = async (_, { token }, { client }: Context) => { + try { + let csrfSession = await getCsrfSession(); + let { cookies, ...hpChallenge } = await getHpChallenge(csrfSession); + + return authenticateActivateAccount({ + initialSessionCookie: cookies, + csrf: csrfSession.csrf, + client, + emailToken: token, + ...hpChallenge, + }); + } catch (unknownError) { + const error = unknownError as Error; + throw new Error(`activate-account: ${error.message}`); + } +}; + +export let activateAccountMutation = mutationField('activateAccount', { + type: 'LoginOutput', + args: { + token: stringArg(), + }, + resolve: activateAccountMutationResolver, +}); diff --git a/api/src/resolvers/auth/authenticateLoginLink.ts b/api/src/resolvers/auth/authenticateLoginLink.ts new file mode 100644 index 00000000..715591ec --- /dev/null +++ b/api/src/resolvers/auth/authenticateLoginLink.ts @@ -0,0 +1,32 @@ +import { FieldResolver, mutationField, stringArg } from 'nexus'; + +import { authenticateLoginLink, getCsrfSession } from '../../helpers'; +import { Context } from '../../types'; + +export let authenticateLoginLinkMutationResolver: FieldResolver< + 'Mutation', + 'authenticateLoginLink' +> = async (_, { token }, { client }: Context) => { + try { + let csrfSession = await getCsrfSession(); + return authenticateLoginLink({ + ...csrfSession, + emailToken: token, + client, + }); + } catch (unknownError) { + const error = unknownError as Error; + throw new Error(`LoginError: ${error.message}`); + } +}; + +export let authenticateLoginLinkMutation = mutationField( + 'authenticateLoginLink', + { + type: 'LoginOutput', + args: { + token: stringArg(), + }, + resolve: authenticateLoginLinkMutationResolver, + }, +); diff --git a/api/src/resolvers/auth/loginWithAppleMutation.ts b/api/src/resolvers/auth/loginWithAppleMutation.ts new file mode 100644 index 00000000..500138bf --- /dev/null +++ b/api/src/resolvers/auth/loginWithAppleMutation.ts @@ -0,0 +1,24 @@ +import { FieldResolver, mutationField, stringArg } from 'nexus'; + +import { authenticateApple, getCsrfSession } from '../../helpers'; +import { Context } from '../../types'; + +export let loginWithAppleMutationResolver: FieldResolver< + 'Mutation', + 'loginWithApple' +> = async (_, { identityToken }, { client }: Context) => { + let csrfSession = await getCsrfSession(); + return authenticateApple({ + ...csrfSession, + identityToken, + client, + }); +}; + +export let loginWithAppleMutation = mutationField('loginWithApple', { + type: 'LoginOutput', + args: { + identityToken: stringArg(), + }, + resolve: loginWithAppleMutationResolver, +}); diff --git a/api/src/resolvers/auth/requestLoginLinkMutation.ts b/api/src/resolvers/auth/requestLoginLinkMutation.ts new file mode 100644 index 00000000..e78c5033 --- /dev/null +++ b/api/src/resolvers/auth/requestLoginLinkMutation.ts @@ -0,0 +1,46 @@ +import { stringify } from 'querystring'; + +import { FieldResolver, mutationField, stringArg } from 'nexus'; + +import { errorHandler, getCsrfSession } from '../../helpers'; +import { Context } from '../../types'; +import { ACCEPTED_LANGUAGE, CONTENT_FORM_URLENCODED } from '../../constants'; + +export let requestLoginLinkMutationResolver: FieldResolver< + 'Mutation', + 'requestLoginLink' +> = async (_, { login }, { client }: Context) => { + let { csrf, initialSessionCookie } = await getCsrfSession(); + const config = { + headers: { + 'Accept-Language': ACCEPTED_LANGUAGE, + 'Content-Type': CONTENT_FORM_URLENCODED, + 'x-csrf-token': csrf, + }, + withCredentials: true, + Cookie: initialSessionCookie, + }; + let body = { + login, + }; + + try { + let { data } = await client.post(`/u/email-login`, stringify(body), config); + + if (data.user_found) { + return 'success'; + } else { + throw new Error(`No account matches ${login}`); + } + } catch (e) { + throw errorHandler(e); + } +}; + +export let requestLoginLinkMutation = mutationField('requestLoginLink', { + type: 'String', + args: { + login: stringArg(), + }, + resolve: requestLoginLinkMutationResolver, +}); diff --git a/api/src/resolvers/index.ts b/api/src/resolvers/index.ts index 0334dd99..d9eebbbd 100644 --- a/api/src/resolvers/index.ts +++ b/api/src/resolvers/index.ts @@ -3,6 +3,10 @@ export * from './auth/loginMutation'; export * from './auth/logoutMutation'; export * from './auth/refreshTokenQuery'; export * from './auth/registerMutation'; +export * from './auth/loginWithAppleMutation'; +export * from './auth/activateAccountMutation'; +export * from './auth/requestLoginLinkMutation'; +export * from './auth/authenticateLoginLink'; export * from './email/addEmailMutation'; export * from './email/changeEmailMutation'; @@ -15,6 +19,7 @@ export * from './notifications/pushNotificationMutation'; export * from './site/aboutQuery'; export * from './site/siteQuery'; +export * from './site/pluginStatusQuery'; export * from './topics/bookmarkPostMutation'; export * from './topics/categoryQuery'; diff --git a/api/src/resolvers/site/aboutQuery.ts b/api/src/resolvers/site/aboutQuery.ts index aa76e836..8718e15d 100644 --- a/api/src/resolvers/site/aboutQuery.ts +++ b/api/src/resolvers/site/aboutQuery.ts @@ -11,17 +11,28 @@ let aboutResolver: FieldResolver<'Query', 'about'> = async ( try { let siteUrl = `/about.json`; + /** + * In here when use newest version discourse from 3.2.0.beta4-dev the name of field change into topics_count and posts_count + * + * And for the previous version discourse it use topic_count and post_count + */ + let { data: { about: { - stats: { topic_count: topicCount, post_count: postCount }, + stats: { + topics_count: topicsCount, + topic_count: topicCount, + posts_count: postsCount, + post_count: postCount, + }, }, }, } = await context.client.get(siteUrl); return { - topicCount, - postCount, + topicCount: topicCount || topicsCount, + postCount: postCount || postsCount, }; } catch (error) { throw errorHandler(error); diff --git a/api/src/resolvers/site/pluginStatusQuery.ts b/api/src/resolvers/site/pluginStatusQuery.ts new file mode 100644 index 00000000..d032bdcf --- /dev/null +++ b/api/src/resolvers/site/pluginStatusQuery.ts @@ -0,0 +1,37 @@ +import { FieldResolver, queryField } from 'nexus'; + +import { errorHandler } from '../../helpers'; +import { Context } from '../../types'; +import { ACCEPTED_LANGUAGE, CONTENT_JSON } from '../../constants'; + +let pluginStatusResolver: FieldResolver<'Query', 'pluginStatus'> = async ( + _, + __, + context: Context, +) => { + try { + const config = { + headers: { + 'Accept-Language': ACCEPTED_LANGUAGE, + 'Content-Type': CONTENT_JSON, + }, + }; + let { + data: { apple, loginLink }, + } = await context.client.get(`/lexicon/auth/status.json`, config); + + return { + appleLoginEnabled: apple, + loginLinkEnabled: loginLink, + }; + } catch (error) { + throw errorHandler(error); + } +}; + +let pluginStatusQuery = queryField('pluginStatus', { + type: 'PluginStatus', + resolve: pluginStatusResolver, +}); + +export { pluginStatusQuery }; diff --git a/api/src/resolvers/site/siteQuery.ts b/api/src/resolvers/site/siteQuery.ts index 09875269..cedef727 100644 --- a/api/src/resolvers/site/siteQuery.ts +++ b/api/src/resolvers/site/siteQuery.ts @@ -23,6 +23,7 @@ let siteResolver: FieldResolver<'Query', 'site'> = async ( post_action_types: postActionTypes, uncategorized_category_id: uncategorizedCategoryId = UNCATEGORIZED_CATEGORY_ID, + lexicon, ...siteData }, } = await context.client.get(siteUrl); @@ -90,6 +91,8 @@ let siteResolver: FieldResolver<'Query', 'site'> = async ( discourseBaseUrl: PROSE_DISCOURSE_HOST || '', allowPoll, pollCreateMinimumTrustLevel, + enableLexiconPushNotifications: + lexicon?.settings.lexicon_push_notifications_enabled || false, ...camelcaseKey(siteData, { deep: true }), }; } catch (error) { diff --git a/api/src/resolvers/topics/likeTopicOrPostMutation.ts b/api/src/resolvers/topics/likeTopicOrPostMutation.ts index 08bc32a0..f84fd668 100644 --- a/api/src/resolvers/topics/likeTopicOrPostMutation.ts +++ b/api/src/resolvers/topics/likeTopicOrPostMutation.ts @@ -20,7 +20,10 @@ import { fetchTopicDetail, fetchPost, } from '../../helpers'; -import { LikableEntity, likeErrorHandler } from '../../helpers/likeErroHandler'; +import { + LikableEntity, + likeErrorHandler, +} from '../../helpers/likeErrorHandler'; import { ActionsSummary, Context, LikedTopic } from '../../types'; export let likeTopicOrPostResolver: FieldResolver< diff --git a/api/src/resolvers/upload/uploadMutation.ts b/api/src/resolvers/upload/uploadMutation.ts index 790fed4e..840dad7a 100644 --- a/api/src/resolvers/upload/uploadMutation.ts +++ b/api/src/resolvers/upload/uploadMutation.ts @@ -1,6 +1,7 @@ import camelcaseKeys from 'camelcase-keys'; import FormData from 'form-data'; import { FieldResolver, mutationField, arg, intArg, nullable } from 'nexus'; +import sharp from 'sharp'; import { errorHandler } from '../../helpers'; import { Context } from '../../types'; @@ -17,7 +18,16 @@ export let uploadResolver: FieldResolver<'Mutation', 'upload'> = async ( const fileBuffer = Buffer.from(await file.arrayBuffer()); - form.append('files[]', fileBuffer, file.name); + let resizedImageBuffer = fileBuffer; + + /** + * This condition to optimize file image if more than 1 Mb to use sharp which will resize file size to be optimal + */ + if (file.size > 1000000 && type === 'avatar') { + resizedImageBuffer = await sharp(fileBuffer).toBuffer(); + } + + form.append('files[]', resizedImageBuffer, file.name); form.append('type', type); if (userId) { form.append('user_id', userId); diff --git a/api/src/scalars/PosterOutputUnion.ts b/api/src/scalars/PosterOutputUnion.ts index ef350c01..2b259ce4 100644 --- a/api/src/scalars/PosterOutputUnion.ts +++ b/api/src/scalars/PosterOutputUnion.ts @@ -3,11 +3,11 @@ import { unionType } from 'nexus'; export let PosterOutputUnion = unionType({ name: 'PosterOutputUnion', definition(t) { - t.members('TopicPoster', 'SuggestionTopicPoster'); + t.members('TopicPosterNewUnion', 'SuggestionTopicPoster'); }, resolveType: (item) => { if (item.hasOwnProperty('userId')) { - return 'TopicPoster'; + return 'TopicPosterNewUnion'; } return 'SuggestionTopicPoster'; }, diff --git a/api/src/typeSchemas/LoginOutput.ts b/api/src/typeSchemas/LoginOutput.ts index 7751c4d6..406241b6 100644 --- a/api/src/typeSchemas/LoginOutput.ts +++ b/api/src/typeSchemas/LoginOutput.ts @@ -11,5 +11,6 @@ export let LoginOutput = objectType({ t.field('user', { type: 'UserLite' }); // Auth t.string('token'); + t.boolean('enableLexiconPushNotifications'); }, }); diff --git a/api/src/typeSchemas/PluginStatus.ts b/api/src/typeSchemas/PluginStatus.ts new file mode 100644 index 00000000..2fb4443a --- /dev/null +++ b/api/src/typeSchemas/PluginStatus.ts @@ -0,0 +1,9 @@ +import { objectType } from 'nexus'; + +export let PluginStatus = objectType({ + name: 'PluginStatus', + definition(t) { + t.boolean('appleLoginEnabled'); + t.boolean('loginLinkEnabled'); + }, +}); diff --git a/api/src/typeSchemas/SiteSetting.ts b/api/src/typeSchemas/SiteSetting.ts index 981d84c7..64d61afc 100644 --- a/api/src/typeSchemas/SiteSetting.ts +++ b/api/src/typeSchemas/SiteSetting.ts @@ -42,6 +42,12 @@ export let SiteSetting = objectType({ t.list.field('groups', { type: 'GroupSiteSetting', }); + + /** + * This field for check plugin + */ + + t.boolean('enableLexiconPushNotifications'); }, }); diff --git a/api/src/typeSchemas/Topic.ts b/api/src/typeSchemas/Topic.ts index 3c377dc8..b924be86 100644 --- a/api/src/typeSchemas/Topic.ts +++ b/api/src/typeSchemas/Topic.ts @@ -50,7 +50,32 @@ export let Topic = objectType({ t.nullable.boolean('pinnedGlobally'); t.nullable.boolean('hasSummary'); - t.list.field('posters', { type: 'PosterOutputUnion' }); + /** + * Deprecated posters type which will use postersUnion type for return posters + */ + t.list.field('posters', { type: 'TopicPoster' }); + t.nullable.list.field('postersUnion', { + type: 'PosterOutputUnion', + resolve: ({ posters }) => { + /** + * Which empty data user cannot be happen in here + */ + + let data = posters.map((poster) => { + return { + ...poster, + user: poster.user || { + avatarTemplate: '', + id: 0, + + username: '', + }, + }; + }); + return data; + }, + }); + t.nullable.list.field('participants', { type: 'MessageParticipant', }); diff --git a/api/src/typeSchemas/TopicPoster.ts b/api/src/typeSchemas/TopicPoster.ts index a061081e..04f06b94 100644 --- a/api/src/typeSchemas/TopicPoster.ts +++ b/api/src/typeSchemas/TopicPoster.ts @@ -1,7 +1,22 @@ import { objectType } from 'nexus'; +/** + * Deprecated type TopicPoster + * Which will Union for topic poster + */ + export let TopicPoster = objectType({ name: 'TopicPoster', + definition(t) { + t.nullable.string('extras'); + t.string('description'); + t.nullable.int('userId'); + t.nullable.field('user', { type: 'UserIcon' }); + }, +}); + +export let TopicPosterNewUnion = objectType({ + name: 'TopicPosterNewUnion', definition(t) { t.nullable.string('extras'); t.string('description'); diff --git a/api/src/typeSchemas/UserActions.ts b/api/src/typeSchemas/UserActions.ts index a3aea65c..fde81a05 100644 --- a/api/src/typeSchemas/UserActions.ts +++ b/api/src/typeSchemas/UserActions.ts @@ -1,6 +1,7 @@ import { objectType } from 'nexus'; import { getNormalizedUrlTemplate } from '../resolvers/utils'; +import { userActivityMarkdownContent } from '../helpers'; export let UserActions = objectType({ name: 'UserActions', @@ -37,5 +38,10 @@ export let UserActions = objectType({ t.int('topicId'); t.int('userId'); t.string('username'); + t.nullable.string('markdownContent', { + resolve: ({ excerpt }) => { + return userActivityMarkdownContent(excerpt); + }, + }); }, }); diff --git a/api/src/typeSchemas/index.ts b/api/src/typeSchemas/index.ts index 16b653ca..3d3b433d 100644 --- a/api/src/typeSchemas/index.ts +++ b/api/src/typeSchemas/index.ts @@ -79,3 +79,4 @@ export * from './UserProfileOutput'; export * from './UserTopic'; export * from './HealthCheck'; export * from './UserStatus'; +export * from './PluginStatus'; diff --git a/api/src/types/dataTypes.ts b/api/src/types/dataTypes.ts index 17b179d7..d5f62dbd 100644 --- a/api/src/types/dataTypes.ts +++ b/api/src/types/dataTypes.ts @@ -29,9 +29,18 @@ export const UserIcon = z.object({ export type UserIcon = z.infer; -// TODO: #1174: get to the bottom of why we have both `userId` and -// `user`, and why both can be nullable. Seems we made a mistake somewhere. +/** + * Deprecated TopicPoster type which will use TopicPosterNewUnion type + */ + export const TopicPoster = z.object({ + extras: z.optional(z.nullable(z.string())), + description: z.string(), + userId: z.optional(z.nullable(z.number())), + user: z.optional(z.nullable(UserIcon)), +}); + +export const TopicPosterNewUnion = z.object({ extras: z.optional(z.nullable(z.string())), description: z.string(), userId: z.number(), @@ -44,8 +53,12 @@ export const SuggestionTopicPoster = z.object({ user: UserIcon, }); -export const PosterUnion = z.union([TopicPoster, SuggestionTopicPoster]); +export const PosterUnion = z.union([ + TopicPosterNewUnion, + SuggestionTopicPoster, +]); +export type TopicPoster = z.infer; export type PosterUnion = z.infer; export type Topic = { diff --git a/api/yarn.lock b/api/yarn.lock index 450661eb..feebf6e2 100644 --- a/api/yarn.lock +++ b/api/yarn.lock @@ -1336,6 +1336,11 @@ axios@^0.21.2: dependencies: follow-redirects "^1.14.0" +b4a@^1.6.4: + version "1.6.4" + resolved "https://registry.yarnpkg.com/b4a/-/b4a-1.6.4.tgz#ef1c1422cae5ce6535ec191baeed7567443f36c9" + integrity sha512-fpWrvyVHEKyeEvbKZTVOeZF3VSKKWtJxFIxX/jaVPf+cLbGUSitjb49pHLqPV2BUNNZ0LcoeEGfE/YCpyDYHIw== + babel-jest@^26.6.3: version "26.6.3" resolved "https://registry.npmjs.org/babel-jest/-/babel-jest-26.6.3.tgz" @@ -1402,6 +1407,11 @@ balanced-match@^1.0.0: resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz" integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== +base64-js@^1.3.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" + integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== + base@^0.11.1: version "0.11.2" resolved "https://registry.npmjs.org/base/-/base-0.11.2.tgz" @@ -1420,6 +1430,15 @@ binary-extensions@^2.0.0: resolved "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz" integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== +bl@^4.0.3: + version "4.1.0" + resolved "https://registry.yarnpkg.com/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a" + integrity sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w== + dependencies: + buffer "^5.5.0" + inherits "^2.0.4" + readable-stream "^3.4.0" + brace-expansion@^1.1.7: version "1.1.11" resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz" @@ -1485,6 +1504,14 @@ buffer-from@1.x, buffer-from@^1.0.0: resolved "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz" integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== +buffer@^5.5.0: + version "5.7.1" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0" + integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ== + dependencies: + base64-js "^1.3.1" + ieee754 "^1.1.13" + busboy@^1.6.0: version "1.6.0" resolved "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz" @@ -1588,6 +1615,11 @@ chokidar@^3.5.1: optionalDependencies: fsevents "~2.3.2" +chownr@^1.1.1: + version "1.1.4" + resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b" + integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg== + ci-info@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz" @@ -1659,7 +1691,7 @@ color-name@^1.0.0, color-name@~1.1.4: resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== -color-string@^1.6.0: +color-string@^1.6.0, color-string@^1.9.0: version "1.9.1" resolved "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz" integrity sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg== @@ -1675,6 +1707,14 @@ color@^3.1.3: color-convert "^1.9.3" color-string "^1.6.0" +color@^4.2.3: + version "4.2.3" + resolved "https://registry.yarnpkg.com/color/-/color-4.2.3.tgz#d781ecb5e57224ee43ea9627560107c0e0c6463a" + integrity sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A== + dependencies: + color-convert "^2.0.1" + color-string "^1.9.0" + colorspace@1.1.x: version "1.1.4" resolved "https://registry.npmjs.org/colorspace/-/colorspace-1.1.4.tgz" @@ -1802,9 +1842,21 @@ decimal.js@^10.2.1: integrity sha512-Nv6ENEzyPQ6AItkGwLE2PGKinZZ9g59vSh2BeH6NqPu0OTKZ5ruJsVqh/orbAnqXc9pBbgXAIrc2EyaCj8NpGg== decode-uri-component@^0.2.0: - version "0.2.0" - resolved "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz" - integrity sha512-hjf+xovcEn31w/EUYdTXQh/8smFL/dzYjohQGEIgjyNavaJfBY2p5F527Bo1VPATxv0VYTUC2bOcXvqFwk78Og== + version "0.2.2" + resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.2.tgz#e69dbe25d37941171dd540e024c444cd5188e1e9" + integrity sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ== + +decompress-response@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-6.0.0.tgz#ca387612ddb7e104bd16d85aab00d5ecf09c66fc" + integrity sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ== + dependencies: + mimic-response "^3.1.0" + +deep-extend@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" + integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA== deep-is@^0.1.3, deep-is@~0.1.3: version "0.1.4" @@ -1851,6 +1903,11 @@ delayed-stream@~1.0.0: resolved "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz" integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== +detect-libc@^2.0.0, detect-libc@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.2.tgz#8ccf2ba9315350e1241b88d0ac3b0e1fbd99605d" + integrity sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw== + detect-newline@^3.0.0: version "3.1.0" resolved "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz" @@ -1931,7 +1988,7 @@ enabled@2.0.x: resolved "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz" integrity sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ== -end-of-stream@^1.1.0: +end-of-stream@^1.1.0, end-of-stream@^1.4.1: version "1.4.4" resolved "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz" integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== @@ -2321,6 +2378,11 @@ expand-brackets@^2.1.4: snapdragon "^0.8.1" to-regex "^3.0.1" +expand-template@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/expand-template/-/expand-template-2.0.3.tgz#6e14b3fcee0f3a6340ecb57d2e8918692052a47c" + integrity sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg== + expect@^26.6.2: version "26.6.2" resolved "https://registry.npmjs.org/expect/-/expect-26.6.2.tgz" @@ -2372,6 +2434,11 @@ fast-diff@^1.1.2: resolved "https://registry.npmjs.org/fast-diff/-/fast-diff-1.2.0.tgz" integrity sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w== +fast-fifo@^1.1.0, fast-fifo@^1.2.0: + version "1.3.2" + resolved "https://registry.yarnpkg.com/fast-fifo/-/fast-fifo-1.3.2.tgz#286e31de96eb96d38a97899815740ba2a4f3640c" + integrity sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ== + fast-glob@^3.2.9: version "3.2.11" resolved "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.11.tgz" @@ -2470,9 +2537,9 @@ fn.name@1.x.x: integrity sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw== follow-redirects@^1.14.0: - version "1.15.1" - resolved "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.1.tgz" - integrity sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA== + version "1.15.6" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.6.tgz#7f815c0cda4249c74ff09e95ef97c23b5fd0399b" + integrity sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA== for-in@^1.0.2: version "1.0.2" @@ -2517,6 +2584,11 @@ fragment-cache@^0.2.1: dependencies: map-cache "^0.2.2" +fs-constants@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad" + integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow== + fs.realpath@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz" @@ -2608,6 +2680,11 @@ get-value@^2.0.3, get-value@^2.0.6: resolved "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz" integrity sha512-Ln0UQDlxH1BapMu3GPtf7CuYNwRZf2gwCuPqbyG6pB8WfmFpzqcy4xtAaAMUhnNqjMKTiCPZG2oMT3YSx8U2NA== +github-from-package@0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/github-from-package/-/github-from-package-0.0.0.tgz#97fb5d96bfde8973313f20e8288ef9a167fa64ce" + integrity sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw== + glob-parent@^5.1.2, glob-parent@~5.1.2: version "5.1.2" resolved "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz" @@ -2784,6 +2861,11 @@ iconv-lite@0.4.24: dependencies: safer-buffer ">= 2.1.2 < 3" +ieee754@^1.1.13: + version "1.2.1" + resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" + integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== + ignore@^4.0.6: version "4.0.6" resolved "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz" @@ -2823,11 +2905,16 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@^2.0.3: +inherits@2, inherits@^2.0.3, inherits@^2.0.4: version "2.0.4" resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== +ini@~1.3.0: + version "1.3.8" + resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" + integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== + internal-slot@^1.0.3: version "1.0.3" resolved "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.3.tgz" @@ -3830,6 +3917,11 @@ mimic-fn@^2.1.0: resolved "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz" integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== +mimic-response@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-3.1.0.tgz#2d1d59af9c1b129815accc2c46a022a5ce1fa3c9" + integrity sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ== + minimatch@^3.0.4, minimatch@^3.1.1, minimatch@^3.1.2: version "3.1.2" resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz" @@ -3842,6 +3934,11 @@ minimist@^1.1.1, minimist@^1.2.0, minimist@^1.2.5, minimist@^1.2.6: resolved "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz" integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q== +minimist@^1.2.3: + version "1.2.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" + integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== + mixin-deep@^1.2.0: version "1.3.2" resolved "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz" @@ -3850,6 +3947,11 @@ mixin-deep@^1.2.0: for-in "^1.0.2" is-extendable "^1.0.1" +mkdirp-classic@^0.5.2, mkdirp-classic@^0.5.3: + version "0.5.3" + resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113" + integrity sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A== + mkdirp@1.x, mkdirp@^1.0.4: version "1.0.4" resolved "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz" @@ -3892,6 +3994,11 @@ nanomatch@^1.2.9: snapdragon "^0.8.1" to-regex "^3.0.1" +napi-build-utils@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/napi-build-utils/-/napi-build-utils-1.0.2.tgz#b1fddc0b2c46e380a0b7a76f984dd47c41a13806" + integrity sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg== + natural-compare@^1.4.0: version "1.4.0" resolved "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz" @@ -3910,6 +4017,18 @@ nice-try@^1.0.4: resolved "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz" integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== +node-abi@^3.3.0: + version "3.51.0" + resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-3.51.0.tgz#970bf595ef5a26a271307f8a4befa02823d4e87d" + integrity sha512-SQkEP4hmNWjlniS5zdnfIXTk1x7Ome85RDzHlTbBtzE97Gfwz/Ipw4v/Ryk20DWIy3yCNVLVlGKApCnmvYoJbA== + dependencies: + semver "^7.3.5" + +node-addon-api@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-6.1.0.tgz#ac8470034e58e67d0c6f1204a18ae6995d9c0d76" + integrity sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA== + node-domexception@1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz" @@ -4236,6 +4355,24 @@ posix-character-classes@^0.1.0: resolved "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz" integrity sha512-xTgYBc3fuo7Yt7JbiuFxSYGToMoz8fLoE6TC9Wx1P/u+LfeThMOAqmuyECnlBaaJb+u1m9hHiXUEtwW4OzfUJg== +prebuild-install@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-7.1.1.tgz#de97d5b34a70a0c81334fd24641f2a1702352e45" + integrity sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw== + dependencies: + detect-libc "^2.0.0" + expand-template "^2.0.3" + github-from-package "0.0.0" + minimist "^1.2.3" + mkdirp-classic "^0.5.3" + napi-build-utils "^1.0.1" + node-abi "^3.3.0" + pump "^3.0.0" + rc "^1.2.7" + simple-get "^4.0.0" + tar-fs "^2.0.0" + tunnel-agent "^0.6.0" + prelude-ls@^1.2.1: version "1.2.1" resolved "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz" @@ -4335,11 +4472,26 @@ queue-microtask@^1.2.2: resolved "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz" integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== +queue-tick@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/queue-tick/-/queue-tick-1.0.1.tgz#f6f07ac82c1fd60f82e098b417a80e52f1f4c142" + integrity sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag== + quick-lru@^4.0.1: version "4.0.1" resolved "https://registry.npmjs.org/quick-lru/-/quick-lru-4.0.1.tgz" integrity sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g== +rc@^1.2.7: + version "1.2.8" + resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" + integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw== + dependencies: + deep-extend "^0.6.0" + ini "~1.3.0" + minimist "^1.2.0" + strip-json-comments "~2.0.1" + react-is@^16.13.1: version "16.13.1" resolved "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz" @@ -4369,7 +4521,7 @@ read-pkg@^5.2.0: parse-json "^5.0.0" type-fest "^0.6.0" -readable-stream@^3.4.0, readable-stream@^3.6.0: +readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.6.0: version "3.6.2" resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz" integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA== @@ -4518,16 +4670,16 @@ run-parallel@^1.1.9: dependencies: queue-microtask "^1.2.2" +safe-buffer@^5.0.1, safe-buffer@~5.2.0: + version "5.2.1" + resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + safe-buffer@~5.1.1: version "5.1.2" resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz" integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== -safe-buffer@~5.2.0: - version "5.2.1" - resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz" - integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== - safe-regex@^1.1.0: version "1.1.0" resolved "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz" @@ -4572,7 +4724,7 @@ saxes@^5.0.1: resolved "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz" integrity sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g== -semver@7.x, semver@^7.2.1, semver@^7.3.2, semver@^7.3.5: +semver@7.x, semver@^7.2.1, semver@^7.3.2, semver@^7.3.5, semver@^7.5.4: version "7.5.4" resolved "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz" integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA== @@ -4604,6 +4756,20 @@ set-value@^2.0.0, set-value@^2.0.1: is-plain-object "^2.0.3" split-string "^3.0.1" +sharp@^0.32.6: + version "0.32.6" + resolved "https://registry.yarnpkg.com/sharp/-/sharp-0.32.6.tgz#6ad30c0b7cd910df65d5f355f774aa4fce45732a" + integrity sha512-KyLTWwgcR9Oe4d9HwCwNM2l7+J0dUQwn/yf7S0EnTtb0eVS4RxO0eUSvxPtzT4F3SY+C4K6fqdv/DO27sJ/v/w== + dependencies: + color "^4.2.3" + detect-libc "^2.0.2" + node-addon-api "^6.1.0" + prebuild-install "^7.1.1" + semver "^7.5.4" + simple-get "^4.0.1" + tar-fs "^3.0.4" + tunnel-agent "^0.6.0" + shebang-command@^1.2.0: version "1.2.0" resolved "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz" @@ -4647,6 +4813,20 @@ signal-exit@^3.0.0, signal-exit@^3.0.2: resolved "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz" integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== +simple-concat@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/simple-concat/-/simple-concat-1.0.1.tgz#f46976082ba35c2263f1c8ab5edfe26c41c9552f" + integrity sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q== + +simple-get@^4.0.0, simple-get@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/simple-get/-/simple-get-4.0.1.tgz#4a39db549287c979d352112fa03fd99fd6bc3543" + integrity sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA== + dependencies: + decompress-response "^6.0.0" + once "^1.3.1" + simple-concat "^1.0.0" + simple-swizzle@^0.2.2: version "0.2.2" resolved "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz" @@ -4813,6 +4993,14 @@ streamsearch@^1.1.0: resolved "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz" integrity sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg== +streamx@^2.15.0: + version "2.15.5" + resolved "https://registry.yarnpkg.com/streamx/-/streamx-2.15.5.tgz#87bcef4dc7f0b883f9359671203344a4e004c7f1" + integrity sha512-9thPGMkKC2GctCzyCUjME3yR03x2xNo0GPKGkRw2UMYN+gqWa9uqpyNWhmsNCutU5zHmkUum0LsCRQTXUgUCAg== + dependencies: + fast-fifo "^1.1.0" + queue-tick "^1.0.1" + string-length@^4.0.1: version "4.0.2" resolved "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz" @@ -4896,7 +5084,7 @@ strip-final-newline@^2.0.0: resolved "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz" integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== -strip-json-comments@^2.0.0: +strip-json-comments@^2.0.0, strip-json-comments@~2.0.1: version "2.0.1" resolved "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz" integrity sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ== @@ -4949,6 +5137,45 @@ table@^6.0.9: string-width "^4.2.3" strip-ansi "^6.0.1" +tar-fs@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.1.1.tgz#489a15ab85f1f0befabb370b7de4f9eb5cbe8784" + integrity sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng== + dependencies: + chownr "^1.1.1" + mkdirp-classic "^0.5.2" + pump "^3.0.0" + tar-stream "^2.1.4" + +tar-fs@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-3.0.4.tgz#a21dc60a2d5d9f55e0089ccd78124f1d3771dbbf" + integrity sha512-5AFQU8b9qLfZCX9zp2duONhPmZv0hGYiBPJsyUdqMjzq/mqVpy/rEUSeHk1+YitmxugaptgBh5oDGU3VsAJq4w== + dependencies: + mkdirp-classic "^0.5.2" + pump "^3.0.0" + tar-stream "^3.1.5" + +tar-stream@^2.1.4: + version "2.2.0" + resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.2.0.tgz#acad84c284136b060dc3faa64474aa9aebd77287" + integrity sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ== + dependencies: + bl "^4.0.3" + end-of-stream "^1.4.1" + fs-constants "^1.0.0" + inherits "^2.0.3" + readable-stream "^3.1.1" + +tar-stream@^3.1.5: + version "3.1.6" + resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-3.1.6.tgz#6520607b55a06f4a2e2e04db360ba7d338cc5bab" + integrity sha512-B/UyjYwPpMBv+PaFSWAmtYjwdrlEaZQEhMIBFNC5oEG8lpiW8XjcSdmEaClj28ArfKScKHs2nshz3k2le6crsg== + dependencies: + b4a "^1.6.4" + fast-fifo "^1.2.0" + streamx "^2.15.0" + terminal-link@^2.0.0: version "2.1.1" resolved "https://registry.npmjs.org/terminal-link/-/terminal-link-2.1.1.tgz" @@ -5155,6 +5382,13 @@ tsutils@^3.21.0: dependencies: tslib "^1.8.1" +tunnel-agent@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" + integrity sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w== + dependencies: + safe-buffer "^5.0.1" + type-check@^0.4.0, type-check@~0.4.0: version "0.4.0" resolved "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz" @@ -5217,9 +5451,9 @@ unbox-primitive@^1.0.2: which-boxed-primitive "^1.0.2" undici@^5.8.0: - version "5.26.3" - resolved "https://registry.yarnpkg.com/undici/-/undici-5.26.3.tgz#ab3527b3d5bb25b12f898dfd22165d472dd71b79" - integrity sha512-H7n2zmKEWgOllKkIUkLvFmsJQj062lSm3uA4EYApG8gLuiOM0/go9bIoC3HVaSnfg4xunowDE2i9p8drkXuvDw== + version "5.28.4" + resolved "https://registry.yarnpkg.com/undici/-/undici-5.28.4.tgz#6b280408edb6a1a604a9b20340f45b422e373068" + integrity sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g== dependencies: "@fastify/busboy" "^2.0.0" diff --git a/documentation/docs/activation-with-link/intro.md b/documentation/docs/activation-with-link/intro.md new file mode 100644 index 00000000..94aa9ce0 --- /dev/null +++ b/documentation/docs/activation-with-link/intro.md @@ -0,0 +1,9 @@ +--- +id: intro +title: Introduction +slug: discourse-plugin/activation-with-link +--- + +The Lexicon Discourse plugin provides support for integrating Discourse's email activation with your Lexicon-powered mobile app. Our plugin modifies links in specific Discourse activation emails account so that when a relevant link is tapped and the user has your Lexicon-powered mobile app installed, it will open the app and automatically activate account and log the user in. + +This section of the documentation offers step-by-step instructions to integrate activation with link into your Discourse site so that your users have a more seamless experience with your Lexicon-powered mobile app. diff --git a/documentation/docs/activation-with-link/setup/enable-activate-with-link.md b/documentation/docs/activation-with-link/setup/enable-activate-with-link.md new file mode 100644 index 00000000..6316850b --- /dev/null +++ b/documentation/docs/activation-with-link/setup/enable-activate-with-link.md @@ -0,0 +1,25 @@ +--- +title: Enabling activation account with link +--- + +import useBaseUrl from '@docusaurus/useBaseUrl'; + +This guide will walk you through the necessary steps to activate activation account with link at lexicon app on your Discourse site. + +## Steps + +1. Access your Discourse admin dashboard. + +2. Navigate to the `Plugins` section. + + + +3. Locate the `discourse-lexicon-plugin` and click on the `Settings` button. + +4. Fill in the `lexicon app scheme` setting with your app scheme. The app scheme is required to enable activation with link. + +5. Check the `lexicon activate account link enabled` box in the Lexicon settings section and save your changes. + + + +Once the activation account with link feature is enabled, you will be able to utilize its functionality in your Discourse instance. diff --git a/documentation/docs/activation-with-link/setup/verify-activate-with-link.md b/documentation/docs/activation-with-link/setup/verify-activate-with-link.md new file mode 100644 index 00000000..d8d6c42f --- /dev/null +++ b/documentation/docs/activation-with-link/setup/verify-activate-with-link.md @@ -0,0 +1,55 @@ +--- +title: Verify Activation Account With Link +--- + +import useBaseUrl from '@docusaurus/useBaseUrl'; + +Below, we'll walk you through how you can validate the functionality of activation account with a link within your Lexicon-powered mobile app. + +:::note +The steps below assume that **you have already build your Lexicon-powered mobile app with the correct app scheme**. If you are running the app on your machine locally through Expo, these steps will not work. + +In order to test account activation with a link, **you will need to use Lexicon version 2.2.0** for your Lexicon app. This feature only works if the user signs up by themselves, not through an invitation from an admin or moderator. Therefore, it is required to disable the **invite only** setting in the Discourse admin settings. + +::: + +:::info +To be able to test this feature, you need an email account that has not been registered in Discourse. + +You also need to be able to log in as an admin on the Discourse website in case you need to approve new users. This is necessary if you have enabled the `must approve users` setting in the Discourse admin settings. + +::: + +## Steps + +To test activation account with link within your Lexicon-powered mobile app, follow these steps: + +1. Ensure that you have Lexicon-powered mobile app at your device. +2. On your mobile device, open your Lexicon-powered mobile app and sign up using new email or you can sign up from discourse website. + > **Note**: + > + > - Ensure that your email client on your mobile device will receive emails for this account. + > - If you want to sign up using mobile app disable Discourse's setting `login required` + +
+ + +
+ +3. After you finish sign up you will receive email to activate account. +4. Open your email on your phone and check the email sent by your Discourse website. + +
+ +
+ +5. Click the link provided in the email. + +
+ +
+ +6. The link will first open in your mobile web browser. When you click `Open App`, if the Lexicon-powered mobile app is installed and matches the configured app scheme, it should automatically open your app and attempt to log you in. + > **Note:** If your admin settings require user approval, the login will fail, and a popup will appear indicating that a moderator's approval is required. + +And that's it! The Lexicon Discourse plugin will properly log you in with a link through your Discourse site. diff --git a/documentation/docs/app-store.md b/documentation/docs/app-store.md index 941750d5..f91b5fa0 100644 --- a/documentation/docs/app-store.md +++ b/documentation/docs/app-store.md @@ -14,6 +14,7 @@ In this page, we'll cover the process of publishing it on iOS. - An Expo account - XCode is installed on your development machine - EAS CLI 2.6.0 or newer +- The [Lexicon Discourse plugin](./discourse-plugin.md) is already installed on your Discourse instance To get started with TestFlight and publishing your app, you'll need an **Apple Developer account**. @@ -91,9 +92,14 @@ First, you'll need to ensure you've set your app name and slug in `frontend/app. Replace these placeholders with your desired values: +:::info +Note below that `scheme` is included. If you want [email deep linking](./email-deep-linking/intro.md) support in your app, **you must specify a scheme**, and then configure the Lexicon Discourse plugin with the same scheme. +::: + ```json - "name": "", - "slug": "", +"name": "", +"slug": "", +"scheme": "", ``` Next, configure EAS Build by running this command from the `frontend/` directory: diff --git a/documentation/docs/concepts.md b/documentation/docs/concepts.md index f32b0652..44211e43 100644 --- a/documentation/docs/concepts.md +++ b/documentation/docs/concepts.md @@ -44,6 +44,8 @@ Having said that, we chose to build Lexicon with it for two primary reasons. We find that Expo makes us much more effective as developers, and also provides excellent services to facilitate the entire process of building and publishing React Native apps. +In particular, Discourse sites that leverage the [Lexicon Discourse Plugin](./discourse-plugin.md) get the benefit of [push notifications](./push-notifications) through Expo's [push notifications service](https://docs.expo.dev/push-notifications/overview/), which abstracts away Google and Apple's push services into a simple interface. + ## Lexicon Architecture The Lexicon Stack is fairly simple, and only consists of 3 major pieces: @@ -51,13 +53,16 @@ The Lexicon Stack is fairly simple, and only consists of 3 major pieces: - The Lexicon Mobile App - The Prose GraphQL API - A running, accessible Discourse instance +- Optionally, you can install our [Discourse Plugin](./discourse-plugin.md) to enable additional features. Below is a diagram illustrating the typical architecture for a Lexicon-powered mobile app. -IOS Lexicon Login Page +IOS Lexicon Login Page As indicated above, the mobile app makes requests to a deployed Prose GraphQL server. The Prose server has been configured to point at an active Discourse instance of the developer's choice. +If the [Lexicon Discourse Plugin](./discourse-plugin.md) is installed, additional endpoints will be exposed which Prose already knows how to communicate with. + Traffic then flows back from Discourse, through Prose, and returns to the mobile app over a GraphQL interface. diff --git a/documentation/docs/discourse-features.md b/documentation/docs/discourse-features.md index 3183014a..fa303920 100644 --- a/documentation/docs/discourse-features.md +++ b/documentation/docs/discourse-features.md @@ -18,35 +18,40 @@ For this reason, most admin tasks are still best accomplished using the Discours ### Lexicon Mobile App Features -| Feature | Description | Supported | Notes | -| ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | --------- | --------------------------------------------------------------------------------------------------------------------- | -| 2FA Login | Allow users with 2FA enabled to be prompted for their 2FA code when logging in | ✅ | Managing 2FA, such as enabling it or disabling it from within the app, is not currently supported | -| Ability to Tag Topics | Create and tag topics to provide relevant metadata for your users | ✅ 🔧 | Configuration required: see [Optimal Experience](optimal#enable-topic-tagging) | -| Topic Previews (Excerpts) | Show an excerpt of the first post in a topic from the Home screen | ✅ 🔧 | Configuration required: see [Optimal Experience](optimal#enable-topic-excerpts) | -| View User Activity | View a user's recent activity—such as topics, posts, and likes—in a single feed from their profile | ✅ | The ability to filter by activity is not currently supported | -| Topic Metrics | Likes, Views, Replies, and Frequent Posters | ✅ | | -| Topic & Post Actions | Ability to like and edit topics and posts | ✅ | | -| View Top & Latest Topics | A Tab View at the top of the main feed provides the ability to switch between Latest and Top activity | ✅ | | -| Search | Search the current Discourse instance for topics and posts based on keywords, categories, and tags | ✅ | | -| Categories | View the category of a topic and filter topics by a given category | ✅ | Categories cannot be created, updated, or deleted | -| Attaching Media to Posts | Users can attach media to a post from the app | ✅ 🔧 | Configuration recommended for supported file extensions-see [Optimal Experience](optimal#configure-upload-extensions) | -| Standard Markdown | Standard Markdown is supported in the editor and rendered correctly in the mobile app | ✅ | Light, incomplete support exists for some of Discourse's custom markup, such as dates | -| Sign Up | Allow users to sign up for an account directly through the mobile app, depending on whether your Discourse instance allows new user registration or not | ✅ | | -| Browsing Public Instances | Allow users to immediately access and browse your Discourse instance from the mobile app if it is not private | ✅ | Users will be prompted to login upon attempting an authenticated action | -| User Profiles | Ability to view users' profiles and edit your own | ✅ | Partial support: displays the user's photo, username, Markdown bio on a single line, and recent activity | -| Post Flagging | Allow users to flag posts for admins to review | ✅ | Admins are not able to review posts in the app, though they will see in-app notifications for flags | -| In-App Notifications | Allow users to see new notifications from the profile screen of the mobile app and mark all notifications as read | ✅ | Some notifications from Discourse are not tappable in the mobile app, such as badge notifications | -| Private messaging | Allow users to start private or group messages with one another | ✅ | | -| Mentions | Allow users to mention a user when creating or editing posts and messages | ✅ | -| Color Scheme | Provides light and dark mode support for users | ✅ | Specify color scheme (light mode, dark mode, or system) from within the app (only local to the user's mobile device) | -| Badges | The ability to see and interact with badges that have been awarded to users on the Discourse instance | ❌ | | -| Post Drafts | Enable users to start composing a draft of a post and return to it later | ❌ | | -| Groups | Enable users to create and participate in private groups of which only group members can view certain topics | ❌ | | -| Admin Features | Discourse admin features generally not available in Lexicon—better suited to a desktop environment | ❌ | Editing posts is supported | -| Post Quotes, Polls, Toggles, and Task Lists | Custom text formatting that enables Discourse-specific features | ❌ | | -| Discourse Emojis | Utilize emojis when creating a topic, making a post, or sending a reply | ❌ | Unicode-based emojis are of course supported | -| Post Bookmarks | Allow users to bookmark certain posts or topics | ❌ | | -| DiscourseConnect (SSO) | Replace Discourse authentication with a Custom Provider | ❌ | | -| Custom Authentication Plugins | Login via OAuth2 or other protocols using custom Discourse Plugins | ❌ | | -| Real-time Chat | Enable users to initiate conversations using the chat feature, either in a channel or through private messaging | ❌ | | -| User Status | Allow other user in community to see user message status | ❌ | | +| Feature | Description | Supported | Notes | +| ------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | --------- | --------------------------------------------------------------------------------------------------------------------- | +| 2FA Login | Allow users with 2FA enabled to be prompted for their 2FA code when logging in | ✅ | Managing 2FA, such as enabling it or disabling it from within the app, is not currently supported | +| Ability to Tag Topics | Create and tag topics to provide relevant metadata for your users | ✅ 🔧 | Configuration required: see [Optimal Experience](optimal#enable-topic-tagging) | +| Topic Previews (Excerpts) | Show an excerpt of the first post in a topic from the Home screen | ✅ 🔧 | Configuration required: see [Optimal Experience](optimal#enable-topic-excerpts) | +| View User Activity | View a user's recent activity—such as topics, posts, and likes—in a single feed from their profile | ✅ | The ability to filter by activity is not currently supported | +| Topic Metrics | Likes, Views, Replies, and Frequent Posters | ✅ | | +| Topic & Post Actions | Ability to like and edit topics and posts | ✅ | | +| View Top & Latest Topics | A Tab View at the top of the main feed provides the ability to switch between Latest and Top activity | ✅ | | +| Search | Search the current Discourse instance for topics and posts based on keywords, categories, and tags | ✅ | | +| Categories | View the category of a topic and filter topics by a given category | ✅ | Categories cannot be created, updated, or deleted | +| Attaching Media to Posts | Users can attach media to a post from the app | ✅ 🔧 | Configuration recommended for supported file extensions-see [Optimal Experience](optimal#configure-upload-extensions) | +| Standard Markdown | Standard Markdown is supported in the editor and rendered correctly in the mobile app | ✅ | Light, incomplete support exists for some of Discourse's custom markup, such as dates | +| Sign Up | Allow users to sign up for an account directly through the mobile app, depending on whether your Discourse instance allows new user registration or not | ✅ | | +| Browsing Public Instances | Allow users to immediately access and browse your Discourse instance from the mobile app if it is not private | ✅ | Users will be prompted to login upon attempting an authenticated action | +| User Profiles | Ability to view users' profiles and edit your own | ✅ | Partial support: displays the user's photo, username, Markdown bio on a single line, and recent activity | +| Post Flagging | Allow users to flag posts for admins to review | ✅ | Admins are not able to review posts in the app, though they will see in-app notifications for flags | +| Mark Discourse Notifications Read | Allow users to see new notifications from the profile screen of the mobile app and mark all notifications as read | ✅ | Some notifications from Discourse are not tappable in the mobile app, such as badge notifications | +| Private messaging | Allow users to start private or group messages with one another | ✅ | | +| Mentions | Allow users to mention a user when creating or editing posts and messages | ✅ | +| Color Scheme | Provides light and dark mode support for users | ✅ | Specify color scheme (light mode, dark mode, or system) from within the app (only local to the user's mobile device) | +| Discourse Emojis | Utilize emojis when creating a topic, making a post, or sending a reply | ✅ | Discourse BB Code emojis and Unicode-based emojis are fully supported. | +| User Status | Allow users to update their statuses and view the statuses of other users | ✅ | | +| Polls | Allow users to create polls with custom settings in posts and private messages. Enable users to view and vote on the polls. | ✅ | | +| Button Bar for Markup Text | Allows users to automatically create Markdown formatting for posts and messages | ✅ | Supports automatic creation of formatting for bold, italic, quoted text, bullet lists, and numbered lists | +| Sign in With Apple | Allows users to log in using their Apple account | ✅ | Apple email account needs to be registered first on Discourse | +| Login With Link | Allows users to log in using an email login link without inserting a password | ✅ | | +| Activation Account With Link | Enables users to log in to the Lexicon-powered mobile app after activating their account upon signing up. Users receive an activation email from Discourse. | ✅ | | +| Badges | The ability to see and interact with badges that have been awarded to users on the Discourse instance | ❌ | | +| Post Drafts | Enable users to start composing a draft of a post and return to it later | ❌ | | +| Groups | Enable users to create and participate in private groups of which only group members can view certain topics | ❌ | | +| Admin Features | Discourse admin features generally not available in Lexicon—better suited to a desktop environment | ❌ | Editing posts is supported | +| Post Quotes, Toggles, and Task Lists | Custom text formatting that enables Discourse-specific features | ❌ | | +| Post Bookmarks | Allow users to bookmark certain posts or topics | ❌ | | +| DiscourseConnect (SSO) | Replace Discourse authentication with a Custom Provider | ❌ | | +| Custom Authentication Plugins | Login via OAuth2 or other protocols using custom Discourse Plugins | ❌ | | +| Real-time Chat | Enable users to initiate conversations using the chat feature, either in a channel or through private messaging | ❌ | | diff --git a/documentation/docs/discourse-plugin-enable.md b/documentation/docs/discourse-plugin-enable.md index 9c032a6a..4308ebc0 100644 --- a/documentation/docs/discourse-plugin-enable.md +++ b/documentation/docs/discourse-plugin-enable.md @@ -15,20 +15,40 @@ After you have confirmed the plugin has been installed and your Discourse instan You'll notice that the `discourse-lexicon-plugin` is not enabled yet. -Plugin Admin Page +Plugin Admin Page 3. Click on the `Settings` button for the `discourse-lexicon-plugin` entry. 4. Select the feature you want to enable and turn it on. -##### Push Notifications +### Push Notifications -For push notifications, all you need to do is check the box for `lexicon push notifications enabled`. This is covered in [Enable Push Notifications](push-notifications/setup/enable-push-notifications). +For push notifications, all you need to do is check the box for `lexicon push notifications enabled`. This is covered in [Enable Push Notifications](./push-notifications/setup/enable-push-notifications.md). -##### Email Deep Linking +### Email Deep Linking For email deep linking, you need to fill in your app scheme first before enabling it. -Plugin Settings Page +Plugin Settings Page This is covered in detail in [Enable Email Deep Linking](./email-deep-linking/setup/enable-email-deep-linking.md). + +### Login With Link + +For Login with Link, you need to fill in your app scheme first before enabling it and check the box for `Lexicon Login Link Enabled`. + +This is covered in detail in [Enable Login With Link](./login-with-link/setup/enable-login-with-link.md). + +### Activation Account With Link + +For activation account with link, you need to fill in your app scheme first before enabling it. + +Plugin Settings Page + +This is covered in detail in [Enable Activation Account With link](./activation-with-link/setup/enable-activate-with-link.md). + +##### Login With Apple + +For Login with Apple, you need to fill in your app bundle ID first before enabling it and check the box for `Lexicon Apple Login Enabled`. + +This is covered in detail in [Enable Login With Apple](./login-with-apple/setup/enable-login-with-apple.md). diff --git a/documentation/docs/discourse-plugin.md b/documentation/docs/discourse-plugin.md index de85ea89..99e31b90 100644 --- a/documentation/docs/discourse-plugin.md +++ b/documentation/docs/discourse-plugin.md @@ -1,5 +1,5 @@ --- -title: Lexicon Discourse Plugin +title: Introduction slug: discourse-plugin/ --- @@ -7,9 +7,17 @@ import useBaseUrl from '@docusaurus/useBaseUrl'; --- -Discourse lacks native mobile app functionalities such as push notifications and deep linking. +As of Lexicon version 2.0.0, a custom Discourse plugin is available to provide a more seamless mobile integration between Discourse and your Lexicon-powered mobile app. -To address this, Lexicon has developed a custom Discourse plugin that seamlessly integrates push notifications and deep linking capabilities. +The plugin offers two key features for version 2.0.0: -- By leveraging Expo's powerful features, our plugin establishes a secure connection between your Discourse site and Expo's push notification service, delivering real-time updates to users' mobile devices based on your site's activity. -- Additionally, our plugin generates custom deep links in email notifications, allowing users seamlessly launch your Lexicon-powered mobile app directly from their mobile email client. +- **Push notifications**: support for native push notifications on user's mobile devices, according to relevant activity on your Discourse site. Powered by Expo's [push notifications service](https://docs.expo.dev/push-notifications/overview/). +- **Email deep linking**: custom deep links in emails from Discourse, allowing users to seamlessly launch your Lexicon-powered mobile app directly from their mobile email client. + +As of Lexicon version 2.2.0, we have added more features to the Discourse Lexicon plugin: + +The plugin now offers three additional features: + +- **Sign in with Apple**: Support for Lexicon-powered mobile apps to enable login using an Apple account. +- **Login with Link**: Support for Lexicon-powered mobile apps to enable login using a link from an email without needing to input a password in the app. [Login with Link documentation](login-with-link/intro.md) +- **Activation with Link**: Support for activating an account after sign-up for Lexicon-powered mobile apps. diff --git a/documentation/docs/email-deep-linking/setup/enable-email-deep-linking.md b/documentation/docs/email-deep-linking/setup/enable-email-deep-linking.md index 73c7dd3f..5e794ddd 100644 --- a/documentation/docs/email-deep-linking/setup/enable-email-deep-linking.md +++ b/documentation/docs/email-deep-linking/setup/enable-email-deep-linking.md @@ -1,5 +1,5 @@ --- -title: Enabling the Lexicon Discourse plugin +title: Enabling the Email Deep Linking --- import useBaseUrl from '@docusaurus/useBaseUrl'; @@ -12,7 +12,7 @@ This guide will walk you through the necessary steps to activate email deep link 2. Navigate to the `Plugins` section. - + 3. Locate the `discourse-lexicon-plugin` and click on the `Settings` button. @@ -20,6 +20,6 @@ This guide will walk you through the necessary steps to activate email deep link 5. Check the `lexicon email deep linking enabled` box in the Lexicon settings section and save your changes. - + Once the email deep linking feature is enabled, you will be able to utilize its functionality in your Discourse instance. diff --git a/documentation/docs/email-deep-linking/setup/verify-email-deep-linking.md b/documentation/docs/email-deep-linking/setup/verify-email-deep-linking.md index 0fe7642d..0d3187d0 100644 --- a/documentation/docs/email-deep-linking/setup/verify-email-deep-linking.md +++ b/documentation/docs/email-deep-linking/setup/verify-email-deep-linking.md @@ -42,8 +42,8 @@ To test email deep linking within your **published** Lexicon-powered mobile app, 1. Click on the button that says `Visit Message` or `Visit Topic`. The label depends on what activity generated the email (see screenshot below). 1. The link will first open in your mobile web browser. Provided that the Lexicon-powered mobile app is installed and matches the configured app scheme, it should automatically open your app to the relevant topic or message scene. -
- +
+
And that's it! You have successfully completed the steps to enable and test email deep linking in your app. diff --git a/documentation/docs/intro.md b/documentation/docs/intro.md index 901f8e22..f358899d 100644 --- a/documentation/docs/intro.md +++ b/documentation/docs/intro.md @@ -44,9 +44,11 @@ Lexicon is a customizable, pre-built mobile app that provides an elegant mobile ## Features -- Topics, Private Messaging, User Signups, Profile Management, and more. -- Straightforward process to [**customize**](white-labeling) the app for your brand +- Topics, Private Messaging, User Signups, Profile Management, and more - Rapidly build Android and iPhone apps for your existing Discourse site +- [Push Notifications](./push-notifications/introduction.md) direct to your users' mobile devices +- More seamless native Discourse experience [with Email Deep Linking](./email-deep-linking/intro.md) +- Straightforward process to [**customize**](white-labeling) the app for your brand - Backed by a [GraphQL](https://graphql.org/) API - Free and open source! - [Commercial support](commercial-support) available diff --git a/documentation/docs/login-with-apple/intro.md b/documentation/docs/login-with-apple/intro.md new file mode 100644 index 00000000..85e5380a --- /dev/null +++ b/documentation/docs/login-with-apple/intro.md @@ -0,0 +1,9 @@ +--- +id: intro +title: Introduction +slug: discourse-plugin/login-with-apple +--- + +The Lexicon Discourse plugin provides support for integrating Apple's authentication with your Lexicon-powered mobile app. Our plugin enables signing into your Discourse site using Apple authentication. + +This section of the documentation offers step-by-step instructions to integrate this login functionality into your Discourse site, providing your users with a more seamless experience with your Lexicon-powered mobile app. diff --git a/documentation/docs/login-with-apple/setup/enable-login-with-apple.md b/documentation/docs/login-with-apple/setup/enable-login-with-apple.md new file mode 100644 index 00000000..18860cbc --- /dev/null +++ b/documentation/docs/login-with-apple/setup/enable-login-with-apple.md @@ -0,0 +1,29 @@ +--- +title: Enabling login with Apple +--- + +import useBaseUrl from '@docusaurus/useBaseUrl'; + +This guide will walk you through the necessary steps to activate login with Apple at lexicon app on your Discourse site. + +## Steps + +1. Access your Discourse admin dashboard. + +2. Navigate to the `Plugins` section. + + + +3. Locate the `discourse-lexicon-plugin` and click on the `Settings` button. + +4. Fill in the `lexicon apple client id` setting with your app bundle ID. The app bundle ID is required to enable login with Apple. If you haven't register an app bundle ID, you can follow the instructions in this [tutorial](../../app-store#register-a-new-bundle-id) to do so. + +
+ +
+ +5. Check the `lexicon apple login enabled` box in the Lexicon settings section and save your changes. + + + +Once the login with Apple feature is enabled, you will be able to utilize its functionality in your Discourse instance. diff --git a/documentation/docs/login-with-apple/setup/verify-login-with-apple.md b/documentation/docs/login-with-apple/setup/verify-login-with-apple.md new file mode 100644 index 00000000..a3212d72 --- /dev/null +++ b/documentation/docs/login-with-apple/setup/verify-login-with-apple.md @@ -0,0 +1,30 @@ +--- +title: Verify Login With Apple +--- + +import useBaseUrl from '@docusaurus/useBaseUrl'; + +Below, we'll walk you through how you can validate the functionality of logging in with Apple within your Lexicon-powered mobile app. + +:::info +In order to be able test logging in with Apple, **you will need to use Lexicon version 2.2.0** for your Lexicon app. +::: + +:::note +Ensure that the Bundle Identifier under iOS section in your `app.json` matches the one in your Discourse's plugin settings. +::: + +## Steps + +To test logging in with Apple within your Lexicon-powered mobile app, follow these steps: + +1. Ensure that you have Lexicon-powered mobile app at your iOS device. +2. On your mobile device, open your Lexicon-powered mobile app. +3. On the login screen, you will see a "Sign in with Apple" button. Click the button and confirm your Apple account. + - **Note**: Ensure that you have a registered account on Discourse using the same email as your Apple account. + +
+ +
+ +And that's it! You will be automatically logged in once your Apple account is confirmed. diff --git a/documentation/docs/login-with-link/intro.md b/documentation/docs/login-with-link/intro.md new file mode 100644 index 00000000..acbd1ec6 --- /dev/null +++ b/documentation/docs/login-with-link/intro.md @@ -0,0 +1,9 @@ +--- +id: intro +title: Introduction +slug: discourse-plugin/login-with-link +--- + +The Lexicon Discourse plugin provides support for integrating Discourse's email login with your Lexicon-powered mobile app. Our plugin modifies links in Discourse login emails so that when a relevant link is tapped, and the user has your Lexicon-powered mobile app installed, it will open the app and automatically log the user in. + +This section of the documentation offers step-by-step instructions to integrate this login functionality into your Discourse site, providing your users with a more seamless experience with your Lexicon-powered mobile app. diff --git a/documentation/docs/login-with-link/setup/enable-login-with-link.md b/documentation/docs/login-with-link/setup/enable-login-with-link.md new file mode 100644 index 00000000..f134ac93 --- /dev/null +++ b/documentation/docs/login-with-link/setup/enable-login-with-link.md @@ -0,0 +1,25 @@ +--- +title: Enabling login with link +--- + +import useBaseUrl from '@docusaurus/useBaseUrl'; + +This guide will walk you through the necessary steps to activate login with link at lexicon app on your Discourse site. + +## Steps + +1. Access your Discourse admin dashboard. + +2. Navigate to the `Plugins` section. + + + +3. Locate the `discourse-lexicon-plugin` and click on the `Settings` button. + +4. Fill in the `lexicon app scheme` setting with your app scheme. The app scheme is required to enable login with linking. + +5. Check the `lexicon login link enabled` box in the Lexicon settings section and save your changes. + + + +Once the login with link feature is enabled, you will be able to utilize its functionality in your Discourse instance. diff --git a/documentation/docs/login-with-link/setup/verify-login-with-link.md b/documentation/docs/login-with-link/setup/verify-login-with-link.md new file mode 100644 index 00000000..50657a0b --- /dev/null +++ b/documentation/docs/login-with-link/setup/verify-login-with-link.md @@ -0,0 +1,39 @@ +--- +title: Verify Login With Link +--- + +import useBaseUrl from '@docusaurus/useBaseUrl'; + +Below, we'll walk you through how you can validate the functionality of login with a link within your Lexicon-powered mobile app. + +:::note +The steps below assume that **you have already build your Lexicon-powered mobile app with the correct app scheme**. If you are running the app on your machine locally through Expo, these steps will not work. +::: + +:::info +In order to be able test login with a link, **you will need to use Lexicon version 2.2.0** for your Lexicon app. +::: + +## Steps + +To test login with a link within your Lexicon-powered mobile app, follow these steps: + +1. Ensure that you have Lexicon-powered mobile app at your device. +2. On your mobile device, open your Lexicon-powered mobile app and log in using one of your accounts. + - **Note**: Ensure that your email client on your mobile device will receive emails for this account. +3. On the login screen, enable `send login link, skip password`. Then, enter your Discourse email account and click the `send link` button. You will receive a popup message to check your email. + +
+ +
+4. Open your email on your phone and check the email sent by your Discourse website. +5. Click the link provided in the email. + +
+ + +
+ +6. The link will first open in your mobile web browser. If the Lexicon-powered mobile app is installed and matches the configured app scheme, it should automatically log you in to your app. + +And that's it! The Lexicon Discourse plugin will properly log you in with a link through your Discourse site. diff --git a/documentation/docs/optimal.md b/documentation/docs/optimal.md index a06199e7..789e1674 100644 --- a/documentation/docs/optimal.md +++ b/documentation/docs/optimal.md @@ -2,7 +2,16 @@ title: 'Optimal Experience' --- -If you're planning to make use of the Lexicon Mobile App, there are a few settings you should tweak on your Discourse instance to provide the best in-app experience to your users. +If you're planning to make use of the Lexicon Mobile App, there are a few adjustments you should make to your Discourse instance to provide the best in-app experience to your users. + +## Install the Lexicon Discourse Plugin + +The Lexicon Discourse plugin enhances the native mobile experience for your users in two key ways: + +- Adds support for push notifications +- Adds support for email deep linking. + +You can read more about the plugin and how to set it up [here](./discourse-plugin.md). ## Enable Topic Excerpts diff --git a/documentation/docs/play-store.md b/documentation/docs/play-store.md index dacdfa8c..f329954e 100644 --- a/documentation/docs/play-store.md +++ b/documentation/docs/play-store.md @@ -13,6 +13,7 @@ If you don't already have a Google Developer account, note that there is a fee t - A [Google Developer Account](https://play.google.com/console/signup) to access the [Google Play Console](https://play.google.com/console) - An Expo account - EAS CLI 2.6.0 or newer +- The [Lexicon Discourse plugin](./discourse-plugin.md) is already installed on your Discourse instance ## Google Play Console @@ -30,9 +31,14 @@ Similar to the approach for [Publishing to the App Store](app-store), if you hav Replace these placeholders with your desired values: +:::info +Note below that `scheme` is included. If you want [email deep linking](./email-deep-linking/intro.md) support in your app, **you must specify a scheme**, and then configure the Lexicon Discourse plugin with the same scheme. +::: + ```json - "name": "", - "slug": "", +"name": "", +"slug": "", +"scheme": "", ``` Then, you need to configure EAS Build by running the following command, or skip to the next [step](play-store#setup-config-values): diff --git a/documentation/docs/push-notifications/setup/enable-push-notifications.md b/documentation/docs/push-notifications/setup/enable-push-notifications.md index e08a9070..7acc5b98 100644 --- a/documentation/docs/push-notifications/setup/enable-push-notifications.md +++ b/documentation/docs/push-notifications/setup/enable-push-notifications.md @@ -1,12 +1,12 @@ --- -title: Enable the Lexicon Discourse plugin +title: Enable Push Notifications --- import useBaseUrl from '@docusaurus/useBaseUrl'; - - + + Below, we'll walk you through the necessary steps to activate push notifications for your Discourse site. @@ -19,13 +19,13 @@ Below, we'll walk you through the necessary steps to activate push notifications 1. Navigate to the Plugins section. - + 4. Click on the `Settings` button for the `discourse-lexicon-plugin` entry. 5. Check the `enable Push Notifications` box in the Lexicon settings section and save your changes. - + Once the push notifications setting is enabled, your users will be able to login through the mobile app and start receiving push notifications. diff --git a/documentation/docs/push-notifications/setup/verify-push-notifications.md b/documentation/docs/push-notifications/setup/verify-push-notifications.md index 26679ffb..7cfe6654 100644 --- a/documentation/docs/push-notifications/setup/verify-push-notifications.md +++ b/documentation/docs/push-notifications/setup/verify-push-notifications.md @@ -5,7 +5,7 @@ title: Verify Push Notifications import useBaseUrl from '@docusaurus/useBaseUrl'; - + Below, we'll walk you through how you can validate the functionality of push notifications within your Lexicon-powered mobile app. @@ -28,6 +28,6 @@ To test push notifications within your Lexicon-powered mobile app, follow these 1. Using a separate account, reply to the post to trigger a notification for the first account. 1. You should receive a push notification on your phone with the reply content from the other account. - + And that's it! The Lexicon Discourse plugin is properly sending push notifications through your Discourse site. diff --git a/documentation/docs/quick-start.md b/documentation/docs/quick-start.md index c72f310c..89f4c8da 100644 --- a/documentation/docs/quick-start.md +++ b/documentation/docs/quick-start.md @@ -6,13 +6,12 @@ title: Quick Start - Node.js 16.14 or newer - The latest version of NPM or Yarn, compatible with Node 16.14 or newer -- Expo CLI 6.0.6 or newer -- EAS CLI 2.6.0 or newer to build and publish the App +- EAS CLI 3.7.2 or newer to build and publish the app - An active Discourse site - If you don’t have one, please follow the instructions in [Development Setup](setup#discourse-host) :::note -Follow the instructions in [Setup Guidance](tutorial/setup) to install the prerequisite depedencies, such as NPM, the Expo CLI, and the EAS CLI. +Follow the instructions in [Setup Guidance](tutorial/setup) to install the prerequisite depedencies, such as NPM and the EAS CLI. ::: ## Installation diff --git a/documentation/docs/setup.md b/documentation/docs/setup.md index 7ef46648..99305a33 100644 --- a/documentation/docs/setup.md +++ b/documentation/docs/setup.md @@ -22,6 +22,16 @@ For detailed instructions on setting up a local development instance of Discours However, if you already have a deployed instance of Discourse, we'd recommend using that instead. +### Install the Lexicon Discourse Plugin + +The Lexicon Discourse Plugin is a Discourse plugin that adds support for [push notifications](./push-notifications/introduction.md) and [email deep linking](./email-deep-linking/intro.md). + +You can install the plugin in your Discourse instance by following the instructions in the [Discourse plugin documentation](./discourse-plugin.md). + +For local development, you're only able to test out push notifications, as email deep linking requires a published app with a [valid app scheme](https://docs.expo.dev/versions/latest/config/app/#scheme). + +If you wish to develop against the plugin itself, you can clone the codebase [here](https://github.com/lexiconhq/discourse-lexicon-plugin.git). + ### Configuration The [Lexicon Stack](concepts#architecture-of-the-lexicon-stack) requires some configuration in order to properly interact with your Discourse server. @@ -125,6 +135,10 @@ In the example above, we have configured the app to point at `https://my-deploye ##### Scenario 2: Run Prose Locally & Access from a Simulator +:::info +If you are running the Prose server locally, you should not expect that the mobile app will continue to function if you turn off your development machine. You must **deploy** the server before attempting to use the mobile app without depending on your development machine. +::: + This approach involves running both the Lexicon Mobile App and the Prose GraphQL API on your development machine. It is accomplished by instructing Expo to launch the Mobile App in the Android or iOS simulator. When developing this way, you can simply set `localDevelopment.proseUrl` to `http://localhost` in `frontend/Config.ts`. And then in `api/.env`, you can set `PROSE_APP_HOSTNAME` to `0.0.0.0`. diff --git a/documentation/docs/supported-devices.md b/documentation/docs/supported-devices.md index 148acbb7..84e6b1bb 100644 --- a/documentation/docs/supported-devices.md +++ b/documentation/docs/supported-devices.md @@ -6,12 +6,16 @@ import useBaseUrl from '@docusaurus/useBaseUrl'; ## iPhone and Android Phones +:::info +Older versions of iOS and Android may work, but are not officially supported. +::: + Once you've published to the App Store and Google Play Store, your published app will work out of the box for your users on both iPhone and Android devices with the following specifications: -| Device | Minimum OS | -| --------------- | ------------------- | -| iPhone | iOS 6 and above | -| Android Devices | Android 5 and above | +| Device | Minimum OS | +| --------------- | -------------------- | +| iPhone | iOS 16 and above | +| Android Devices | Android 13 and above | | Android | iOS | | -------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | diff --git a/documentation/docs/technologies.md b/documentation/docs/technologies.md index 838fcffb..8657288a 100644 --- a/documentation/docs/technologies.md +++ b/documentation/docs/technologies.md @@ -18,8 +18,4 @@ White Label the Lexicon Mobile App to give your users the familiar look and feel Getting started is as easy as spinning up a new server for the Prose GraphQL API, and pointing it at your Discourse instance. No changes are required on your Discourse instance itself. -However, to enable features like [Push Notifications](./push-notifications) and [Email Deep Linking](./email-deep-linking/), you can install our [Discourse Plugin](./discourse-plugin.md). - -However, to provide an [optimal experience](optimal) with features like Tagging and Topic Excerpts, you will need to make some light adjustments. - -This is covered in detail in [Deploying Prose](deployment). +Note: to enable features like [Push Notifications](./push-notifications) and [Email Deep Linking](./email-deep-linking/intro.md), you can install our [Discourse Plugin](./discourse-plugin.md). diff --git a/documentation/docusaurus.config.js b/documentation/docusaurus.config.js index 7a07a24e..ced886ef 100644 --- a/documentation/docusaurus.config.js +++ b/documentation/docusaurus.config.js @@ -64,9 +64,17 @@ module.exports = { editUrl: 'https://github.com/lexiconhq/lexicon/blob/master/documentation/', routeBasePath: '/', - onlyIncludeVersions: ['1.0.0', '2.0.0'], - lastVersion: '2.0.0', + onlyIncludeVersions: ['1.0.0', '2.0.0', '2.1.0', '2.2.0'], + lastVersion: '2.2.0', versions: { + '2.2.0': { + path: 'version-2.2.0', + banner: 'none', + }, + '2.1.0': { + path: 'version-2.1.0', + banner: 'none', + }, '2.0.0': { path: 'version-2.0.0', banner: 'none', @@ -78,7 +86,10 @@ module.exports = { }, }, theme: { - customCss: require.resolve('./src/css/custom.css'), + customCss: [ + require.resolve('./src/css/custom.css'), + require.resolve('./src/css/image.css'), + ], }, }, ], diff --git a/documentation/sidebars.js b/documentation/sidebars.js index e345acf2..7dc4019a 100644 --- a/documentation/sidebars.js +++ b/documentation/sidebars.js @@ -15,13 +15,45 @@ module.exports = { 'White Labeling': ['white-labeling', 'assets', 'theming'], 'Deploying Prose': ['deployment', 'env-prose', 'dedicated'], 'Configuring Discourse': ['optimal'], + 'Discourse Plugin': [ + 'discourse-plugin', + 'discourse-plugin-installation', + 'discourse-plugin-enable', + { + 'Push Notifications': [ + 'push-notifications/introduction', + 'push-notifications/plugin-interaction', + 'push-notifications/setup/enable-push-notifications', + 'push-notifications/setup/verify-push-notifications', + ], + 'Email Deep Linking': [ + 'email-deep-linking/intro', + 'email-deep-linking/setup/enable-email-deep-linking', + 'email-deep-linking/setup/verify-email-deep-linking', + ], + 'Login With Link': [ + 'login-with-link/intro', + 'login-with-link/setup/enable-login-with-link', + 'login-with-link/setup/verify-login-with-link', + ], + 'Activation Account With Link': [ + 'activation-with-link/intro', + 'activation-with-link/setup/enable-activate-with-link', + 'activation-with-link/setup/verify-activate-with-link', + ], + 'Login with Apple': [ + 'login-with-apple/intro', + 'login-with-apple/setup/enable-login-with-apple', + 'login-with-apple/setup/verify-login-with-apple', + ], + }, + ], 'Publishing your App': [ 'app-store', 'play-store', 'lexicon-updates', 'troubleshooting-build', ], - Plugin: ['push-notifications/introduction'], }, tutorial: { Tutorial: [ @@ -37,24 +69,4 @@ module.exports = { 'tutorial/updating', ], }, - plugin: [ - { - type: 'doc', - id: 'push-notifications/introduction', // document ID - label: 'Introduction', // sidebar label - }, - { - type: 'doc', - id: 'push-notifications/plugin-interaction', // document ID - label: 'How Push Notifications work with Lexicon', // sidebar label - }, - { - type: 'category', - label: 'Setup', - items: [ - 'push-notifications/setup/enable-push-notifications', - 'push-notifications/setup/verify-push-notifications', - ], - }, - ], }; diff --git a/documentation/src/css/image.css b/documentation/src/css/image.css new file mode 100644 index 00000000..fb04c3c6 --- /dev/null +++ b/documentation/src/css/image.css @@ -0,0 +1,9 @@ +.image-container-center-multiple { + display: flex; + align-items: center; + justify-content: space-evenly; +} + +.image-container-center { + text-align: center; +} diff --git a/documentation/src/pages/index.js b/documentation/src/pages/index.js index 079a4187..70982829 100644 --- a/documentation/src/pages/index.js +++ b/documentation/src/pages/index.js @@ -2,5 +2,5 @@ import React from 'react'; import { Redirect } from 'react-router-dom'; export default function Home() { - return ; + return ; } diff --git a/documentation/static/img/screenshot/Mobile-LoginWithApple.png b/documentation/static/img/screenshot/Mobile-LoginWithApple.png new file mode 100644 index 00000000..c452c711 Binary files /dev/null and b/documentation/static/img/screenshot/Mobile-LoginWithApple.png differ diff --git a/documentation/static/img/screenshot/Website_SignUp.png b/documentation/static/img/screenshot/Website_SignUp.png new file mode 100644 index 00000000..00b270d6 Binary files /dev/null and b/documentation/static/img/screenshot/Website_SignUp.png differ diff --git a/documentation/static/img/screenshot/Discourse-Plugin-Email-notification.png b/documentation/static/img/screenshot/plugins/Discourse-Plugin-Email-notification.png similarity index 100% rename from documentation/static/img/screenshot/Discourse-Plugin-Email-notification.png rename to documentation/static/img/screenshot/plugins/Discourse-Plugin-Email-notification.png diff --git a/documentation/static/img/screenshot/Discourse-Plugin-EmailDeepLinking-Settings.png b/documentation/static/img/screenshot/plugins/Discourse-Plugin-EmailDeepLinking-Settings.png similarity index 100% rename from documentation/static/img/screenshot/Discourse-Plugin-EmailDeepLinking-Settings.png rename to documentation/static/img/screenshot/plugins/Discourse-Plugin-EmailDeepLinking-Settings.png diff --git a/documentation/static/img/screenshot/Discourse-Plugin-Enable.png b/documentation/static/img/screenshot/plugins/Discourse-Plugin-Enable.png similarity index 100% rename from documentation/static/img/screenshot/Discourse-Plugin-Enable.png rename to documentation/static/img/screenshot/plugins/Discourse-Plugin-Enable.png diff --git a/documentation/static/img/screenshot/Discourse-Plugin-PushNotif-Settings.png b/documentation/static/img/screenshot/plugins/Discourse-Plugin-PushNotif-Settings.png similarity index 100% rename from documentation/static/img/screenshot/Discourse-Plugin-PushNotif-Settings.png rename to documentation/static/img/screenshot/plugins/Discourse-Plugin-PushNotif-Settings.png diff --git a/documentation/static/img/screenshot/Discourse-Plugin-Settings.png b/documentation/static/img/screenshot/plugins/Discourse-Plugin-Settings.png similarity index 100% rename from documentation/static/img/screenshot/Discourse-Plugin-Settings.png rename to documentation/static/img/screenshot/plugins/Discourse-Plugin-Settings.png diff --git a/documentation/static/img/screenshot/Mobile-PushNotification.png b/documentation/static/img/screenshot/plugins/Mobile-PushNotification.png similarity index 100% rename from documentation/static/img/screenshot/Mobile-PushNotification.png rename to documentation/static/img/screenshot/plugins/Mobile-PushNotification.png diff --git a/documentation/static/img/screenshot/plugins/version-2.2.0/Discourse-Plugin-ActivationWithLink-Email.png b/documentation/static/img/screenshot/plugins/version-2.2.0/Discourse-Plugin-ActivationWithLink-Email.png new file mode 100644 index 00000000..eef908b2 Binary files /dev/null and b/documentation/static/img/screenshot/plugins/version-2.2.0/Discourse-Plugin-ActivationWithLink-Email.png differ diff --git a/documentation/static/img/screenshot/plugins/version-2.2.0/Discourse-Plugin-EmailDeepLinking-Settings.png b/documentation/static/img/screenshot/plugins/version-2.2.0/Discourse-Plugin-EmailDeepLinking-Settings.png new file mode 100644 index 00000000..a42f2957 Binary files /dev/null and b/documentation/static/img/screenshot/plugins/version-2.2.0/Discourse-Plugin-EmailDeepLinking-Settings.png differ diff --git a/documentation/static/img/screenshot/plugins/version-2.2.0/Discourse-Plugin-Enable-ActivationWithLink.png b/documentation/static/img/screenshot/plugins/version-2.2.0/Discourse-Plugin-Enable-ActivationWithLink.png new file mode 100644 index 00000000..8e8cd64c Binary files /dev/null and b/documentation/static/img/screenshot/plugins/version-2.2.0/Discourse-Plugin-Enable-ActivationWithLink.png differ diff --git a/documentation/static/img/screenshot/plugins/version-2.2.0/Discourse-Plugin-Enable.png b/documentation/static/img/screenshot/plugins/version-2.2.0/Discourse-Plugin-Enable.png new file mode 100644 index 00000000..0e3c18f6 Binary files /dev/null and b/documentation/static/img/screenshot/plugins/version-2.2.0/Discourse-Plugin-Enable.png differ diff --git a/documentation/static/img/screenshot/plugins/version-2.2.0/Discourse-Plugin-Login-With-Apple-App-ID.png b/documentation/static/img/screenshot/plugins/version-2.2.0/Discourse-Plugin-Login-With-Apple-App-ID.png new file mode 100644 index 00000000..be3d7d27 Binary files /dev/null and b/documentation/static/img/screenshot/plugins/version-2.2.0/Discourse-Plugin-Login-With-Apple-App-ID.png differ diff --git a/documentation/static/img/screenshot/plugins/version-2.2.0/Discourse-Plugin-Login-With-Apple.png b/documentation/static/img/screenshot/plugins/version-2.2.0/Discourse-Plugin-Login-With-Apple.png new file mode 100644 index 00000000..15340886 Binary files /dev/null and b/documentation/static/img/screenshot/plugins/version-2.2.0/Discourse-Plugin-Login-With-Apple.png differ diff --git a/documentation/static/img/screenshot/plugins/version-2.2.0/Discourse-Plugin-Login-With-Link.png b/documentation/static/img/screenshot/plugins/version-2.2.0/Discourse-Plugin-Login-With-Link.png new file mode 100644 index 00000000..acf243b2 Binary files /dev/null and b/documentation/static/img/screenshot/plugins/version-2.2.0/Discourse-Plugin-Login-With-Link.png differ diff --git a/documentation/static/img/screenshot/plugins/version-2.2.0/Discourse-Plugin-LoginWithLink-Email.png b/documentation/static/img/screenshot/plugins/version-2.2.0/Discourse-Plugin-LoginWithLink-Email.png new file mode 100644 index 00000000..826a4b0d Binary files /dev/null and b/documentation/static/img/screenshot/plugins/version-2.2.0/Discourse-Plugin-LoginWithLink-Email.png differ diff --git a/documentation/static/img/screenshot/plugins/version-2.2.0/Discourse-Plugin-PushNotif-Settings.png b/documentation/static/img/screenshot/plugins/version-2.2.0/Discourse-Plugin-PushNotif-Settings.png new file mode 100644 index 00000000..88881314 Binary files /dev/null and b/documentation/static/img/screenshot/plugins/version-2.2.0/Discourse-Plugin-PushNotif-Settings.png differ diff --git a/documentation/static/img/screenshot/plugins/version-2.2.0/Discourse-Plugin-Settings.png b/documentation/static/img/screenshot/plugins/version-2.2.0/Discourse-Plugin-Settings.png new file mode 100644 index 00000000..87f9e059 Binary files /dev/null and b/documentation/static/img/screenshot/plugins/version-2.2.0/Discourse-Plugin-Settings.png differ diff --git a/documentation/static/img/screenshot/plugins/version-2.2.0/Mobile-ActivationWithLink-Redirect.png b/documentation/static/img/screenshot/plugins/version-2.2.0/Mobile-ActivationWithLink-Redirect.png new file mode 100644 index 00000000..f4ee8e9e Binary files /dev/null and b/documentation/static/img/screenshot/plugins/version-2.2.0/Mobile-ActivationWithLink-Redirect.png differ diff --git a/documentation/static/img/screenshot/plugins/version-2.2.0/Mobile-LoginWithLink-Redirect.png b/documentation/static/img/screenshot/plugins/version-2.2.0/Mobile-LoginWithLink-Redirect.png new file mode 100644 index 00000000..f4ee8e9e Binary files /dev/null and b/documentation/static/img/screenshot/plugins/version-2.2.0/Mobile-LoginWithLink-Redirect.png differ diff --git a/documentation/static/img/screenshot/plugins/version-2.2.0/Mobile-LoginWithLink.png b/documentation/static/img/screenshot/plugins/version-2.2.0/Mobile-LoginWithLink.png new file mode 100644 index 00000000..80087c0a Binary files /dev/null and b/documentation/static/img/screenshot/plugins/version-2.2.0/Mobile-LoginWithLink.png differ diff --git a/documentation/versioned_docs/version-1.0.0/discourse-features.md b/documentation/versioned_docs/version-1.0.0/discourse-features.md index 3183014a..5f3ec7cc 100644 --- a/documentation/versioned_docs/version-1.0.0/discourse-features.md +++ b/documentation/versioned_docs/version-1.0.0/discourse-features.md @@ -35,7 +35,7 @@ For this reason, most admin tasks are still best accomplished using the Discours | Browsing Public Instances | Allow users to immediately access and browse your Discourse instance from the mobile app if it is not private | ✅ | Users will be prompted to login upon attempting an authenticated action | | User Profiles | Ability to view users' profiles and edit your own | ✅ | Partial support: displays the user's photo, username, Markdown bio on a single line, and recent activity | | Post Flagging | Allow users to flag posts for admins to review | ✅ | Admins are not able to review posts in the app, though they will see in-app notifications for flags | -| In-App Notifications | Allow users to see new notifications from the profile screen of the mobile app and mark all notifications as read | ✅ | Some notifications from Discourse are not tappable in the mobile app, such as badge notifications | +| Mark Discourse Notifications Read | Allow users to see new notifications from the profile screen of the mobile app and mark all notifications as read | ✅ | Some notifications from Discourse are not tappable in the mobile app, such as badge notifications | | Private messaging | Allow users to start private or group messages with one another | ✅ | | | Mentions | Allow users to mention a user when creating or editing posts and messages | ✅ | | Color Scheme | Provides light and dark mode support for users | ✅ | Specify color scheme (light mode, dark mode, or system) from within the app (only local to the user's mobile device) | diff --git a/documentation/versioned_docs/version-2.0.0/discourse-features.md b/documentation/versioned_docs/version-2.0.0/discourse-features.md index 2de3d0ec..6a5129f1 100644 --- a/documentation/versioned_docs/version-2.0.0/discourse-features.md +++ b/documentation/versioned_docs/version-2.0.0/discourse-features.md @@ -37,7 +37,7 @@ For this reason, most admin tasks are still best accomplished using the Discours | Browsing Public Instances | Allow users to immediately access and browse your Discourse instance from the mobile app if it is not private | ✅ | Users will be prompted to login upon attempting an authenticated action | | User Profiles | Ability to view users' profiles and edit your own | ✅ | Partial support: displays the user's photo, username, Markdown bio on a single line, and recent activity | | Post Flagging | Allow users to flag posts for admins to review | ✅ | Admins are not able to review posts in the app, though they will see in-app notifications for flags | -| In-App Notifications | Allow users to see new notifications from the profile screen of the mobile app and mark all notifications as read | ✅ | Some notifications from Discourse are not tappable in the mobile app, such as badge notifications | +| Mark Discourse Notifications Read | Allow users to see new notifications from the profile screen of the mobile app and mark all notifications as read | ✅ | Some notifications from Discourse are not tappable in the mobile app, such as badge notifications | | Private messaging | Allow users to start private or group messages with one another | ✅ | | | Mentions | Allow users to mention a user when creating or editing posts and messages | ✅ | | Color Scheme | Provides light and dark mode support for users | ✅ | Specify color scheme (light mode, dark mode, or system) from within the app (only local to the user's mobile device) | diff --git a/documentation/versioned_docs/version-2.0.0/discourse-plugin-enable.md b/documentation/versioned_docs/version-2.0.0/discourse-plugin-enable.md index e33e202a..c4b5ff23 100644 --- a/documentation/versioned_docs/version-2.0.0/discourse-plugin-enable.md +++ b/documentation/versioned_docs/version-2.0.0/discourse-plugin-enable.md @@ -15,7 +15,7 @@ After you have confirmed the plugin has been installed and your Discourse instan You'll notice that the `discourse-lexicon-plugin` is not enabled yet. -Plugin Admin Page +Plugin Admin Page 3. Click on the `Settings` button for the `discourse-lexicon-plugin` entry. @@ -29,6 +29,6 @@ For push notifications, all you need to do is check the box for `lexicon push no For email deep linking, you need to fill in your app scheme first before enabling it. -Plugin Settings Page +Plugin Settings Page This is covered in detail in [Enable Email Deep Linking](./email-deep-linking/setup/enable-email-deep-linking.md). diff --git a/documentation/versioned_docs/version-2.0.0/email-deep-linking/setup/enable-email-deep-linking.md b/documentation/versioned_docs/version-2.0.0/email-deep-linking/setup/enable-email-deep-linking.md index 60d0d6bd..ab0340a1 100644 --- a/documentation/versioned_docs/version-2.0.0/email-deep-linking/setup/enable-email-deep-linking.md +++ b/documentation/versioned_docs/version-2.0.0/email-deep-linking/setup/enable-email-deep-linking.md @@ -14,7 +14,7 @@ This guide will walk you through the necessary steps to activate email deep link 1. Navigate to the `Plugins` section. - + 4. Locate the `discourse-lexicon-plugin` and click on the `Settings` button. @@ -22,7 +22,7 @@ This guide will walk you through the necessary steps to activate email deep link 6. Check the `lexicon email deep linking enabled` box in the Lexicon settings section and save your changes. - + Once the email deep linking feature is enabled, you will be able to utilize its functionality in your Discourse instance. diff --git a/documentation/versioned_docs/version-2.0.0/email-deep-linking/setup/verify-email-deep-linking.md b/documentation/versioned_docs/version-2.0.0/email-deep-linking/setup/verify-email-deep-linking.md index 06f51678..8cb537e9 100644 --- a/documentation/versioned_docs/version-2.0.0/email-deep-linking/setup/verify-email-deep-linking.md +++ b/documentation/versioned_docs/version-2.0.0/email-deep-linking/setup/verify-email-deep-linking.md @@ -45,7 +45,7 @@ To test email deep linking within your **published** Lexicon-powered mobile app, 1. The link will first open in your mobile web browser. Provided that the Lexicon-powered mobile app is installed and matches the configured app scheme, it should automatically open your app to the relevant topic or message scene.
- +
And that's it! You have successfully completed the steps to enable and test email deep linking in your app. diff --git a/documentation/versioned_docs/version-2.0.0/push-notifications/setup/enable-push-notifications.md b/documentation/versioned_docs/version-2.0.0/push-notifications/setup/enable-push-notifications.md index adcac398..3a21e465 100644 --- a/documentation/versioned_docs/version-2.0.0/push-notifications/setup/enable-push-notifications.md +++ b/documentation/versioned_docs/version-2.0.0/push-notifications/setup/enable-push-notifications.md @@ -5,8 +5,8 @@ title: Enable Push Notifications import useBaseUrl from '@docusaurus/useBaseUrl'; - - + + Below, we'll walk you through the necessary steps to activate push notifications for your Discourse site. @@ -19,13 +19,13 @@ Below, we'll walk you through the necessary steps to activate push notifications 1. Navigate to the Plugins section. - + 4. Click on the `Settings` button for the `discourse-lexicon-plugin` entry. 5. Check the `enable Push Notifications` box in the Lexicon settings section and save your changes. - + Once the push notifications setting is enabled, your users will be able to login through the mobile app and start receiving push notifications. diff --git a/documentation/versioned_docs/version-2.0.0/push-notifications/setup/verify-push-notifications.md b/documentation/versioned_docs/version-2.0.0/push-notifications/setup/verify-push-notifications.md index 26679ffb..7cfe6654 100644 --- a/documentation/versioned_docs/version-2.0.0/push-notifications/setup/verify-push-notifications.md +++ b/documentation/versioned_docs/version-2.0.0/push-notifications/setup/verify-push-notifications.md @@ -5,7 +5,7 @@ title: Verify Push Notifications import useBaseUrl from '@docusaurus/useBaseUrl'; - + Below, we'll walk you through how you can validate the functionality of push notifications within your Lexicon-powered mobile app. @@ -28,6 +28,6 @@ To test push notifications within your Lexicon-powered mobile app, follow these 1. Using a separate account, reply to the post to trigger a notification for the first account. 1. You should receive a push notification on your phone with the reply content from the other account. - + And that's it! The Lexicon Discourse plugin is properly sending push notifications through your Discourse site. diff --git a/documentation/versioned_docs/version-2.1.0/app-store.md b/documentation/versioned_docs/version-2.1.0/app-store.md new file mode 100644 index 00000000..f91b5fa0 --- /dev/null +++ b/documentation/versioned_docs/version-2.1.0/app-store.md @@ -0,0 +1,282 @@ +--- +title: Publishing to the App Store +--- + +import useBaseUrl from '@docusaurus/useBaseUrl'; + +At this point, you've at least made some minor adjustments to the Lexicon Mobile App, and are ready to publish it so that your users can download it. + +In this page, we'll cover the process of publishing it on iOS. + +## Prerequisites + +- An Apple Developer account +- An Expo account +- XCode is installed on your development machine +- EAS CLI 2.6.0 or newer +- The [Lexicon Discourse plugin](./discourse-plugin.md) is already installed on your Discourse instance + +To get started with TestFlight and publishing your app, you'll need an **Apple Developer account**. + +This will enable you to interact with Apple as you go through the process of submitting to TestFlight and, eventually, the App Store. + +You'll also need an [Expo account](https://expo.dev/signup) so you can build your app, download it, and upload it to Apple's servers. + +Finally, you'll want to have already downloaded and installed [Xcode](https://developer.apple.com/xcode/), which is what you'll use to upload your built app to Apple's servers. + +:::note +If you don't yet have an account with Apple, you'll need to enroll in the [Apple Developer Program](https://developer.apple.com/programs/enroll/) first. Note that there is an annual cost associated with this. + +Additionally, you'll want to make sure you have an account with [Expo](https://expo.dev/signup) so you can use features like [EAS Submit](https://docs.expo.dev/submit/introduction/). +::: + +## Register a new Bundle ID + +Each app in Apple's App Store has a unique **Bundle Identifier**, or Bundle ID. + +In order to publish the app anywhere, including to TestFlight, you'll need to have a Bundle ID registered for your app with Apple. + +Typically, this uses the format of `com..`. + +For example, if your company is named Expo, and your app is named Expo Go, your Bundle ID could be: + +``` +com.expo.expogo +``` + +You can follow these instructions to get one. + +- Go to [Certificates, Identifiers & Profiles](https://developer.apple.com/account/resources/identifiers/bundleId/add/bundle). +- Fill in the following fields, and then click `Continue` + Regsiter App + + - **Description**: You can insert the app name as its description. + + - **Bundle ID**: Select `Explicit`, and then insert then insert your bundle ID in the input field. + +- Capabilities + + - You can leave this section empty. + +## Add a New App in App Store Connect + +Steps: + +- Sign in to your [App Store Connect](https://appstoreconnect.apple.com/) account. +- Click on `My Apps`. + App Connect +- Click on the `+` button to add new app. + Add New App +- Fill out the requested information about your app, and then click `Create`. + Add New App + + - **Platforms**: Select `iOS`. + - **Name**: The name of your app, as it will appear on the App Store and user's devices. + - **Primary Language**: The primary language that will be used if localized app information is not available. + - **Bundle ID**: Choose the Bundle ID you created above. + - **Note**: double-check that it's correct, because you can not change it afterwards. + - **SKU (Stock Keeping Unit)**: A unique ID to differentiate your app from the others, similar to a product ID. + - **User Access**: Full access means all users will have access to the app, while limited access means that the app can only be accessed by certain roles defined within App Store Connect. + +## Configuration + +After creating the app in App Store Connect, you'll want to jump back over to the codebase and make some adjustments. + +### Build Config + +:::note +If you haven't yet installed the EAS CLI, follow the instructions in the [tutorial](tutorial/setup#install-the-eas-cli). +::: + +First, you'll need to ensure you've set your app name and slug in `frontend/app.json`. The [slug](https://docs.expo.dev/workflow/glossary-of-terms/#slug) is used as part of the URL for your app on Expo's web services, so it is recommended to use kebab-case (e.g., `my-lexicon-app`). + +Replace these placeholders with your desired values: + +:::info +Note below that `scheme` is included. If you want [email deep linking](./email-deep-linking/intro.md) support in your app, **you must specify a scheme**, and then configure the Lexicon Discourse plugin with the same scheme. +::: + +```json +"name": "", +"slug": "", +"scheme": "", +``` + +Next, configure EAS Build by running this command from the `frontend/` directory: + +```bash +eas build:configure +``` + +The EAS CLI will prompt you to specify `android.package` and `ios.bundleIdentifier` if those values are not already provided in `app.json`. You'll want to add the bundle ID you just registered in App Store Connect as the `bundleIdentifier`. + +Then you can see that the value has been updated in the `ios` section of `frontend/app.json` file. + +```json + "ios": { + "supportsTablet": false, + "buildNumber": "1.0.0", + "bundleIdentifier": "", + "config": { + "usesNonExemptEncryption" : false + } + }, +``` + +:::note +We set `usesNonExemptEncryption` to `false` because Lexicon doesn't leverage that feature. + +For further details, please take a look at [this link](https://developer.apple.com/documentation/bundleresources/information_property_list/itsappusesnonexemptencryption) from Apple's documentation. +::: + +### Setup Config Values + +:::info +When publishing your app, it is necessary to deploy Prose somewhere publicly accessible, perhaps on a cloud hosting provider like AWS or DigitalOcean. If Prose is only running on your local machine, users that download your app won't be able to use it. +Check [the documentation](deployment) to deploy Prose if you haven't already. +::: + +Next, configure the **Prose URL** for your build in `Config.ts`. You can set a different URL for each build channel. + +:::note +In the original release of Lexicon, the **Prose URL** was specified in `frontend/.env`. However, as part of migrating to Expo's EAS feature, we centralized the configuration into `frontend/Config.ts` to save you the trouble of needing to maintain it in more than one place, as suggested in the [Expo documentation](https://docs.expo.dev/build-reference/variables/#can-i-share-environment-variables-defined-in-easjson-with-expo-start-and-eas-update) +::: + +```ts +const config = { + // ... + buildChannels: { + preview: { + proseUrl: 'http://PLACEHOLDER.change.this.to.your.prose.url', + }, + production: { + proseUrl: 'http://PLACEHOLDER.change.this.to.your.prose.url', + }, + }, +}; +``` + +### Setup Apple Dveloper Account + +Lastly, please adjust these fields in `eas.json` with your account information to submit the app: + +```json + "base": { + "ios": { + "appleId": "", + "ascAppId": "", + "appleTeamId": "" + }, + ... + }, +``` + +- **appleId**: your apple ID (e.g., `john@gmail.com`). +- **ascAppId**: your App Store Connect app ID. Find your ascAppID by following [this guide](https://github.com/expo/fyi/blob/main/asc-app-id.md) (e.g., `1234567890`). +- **appleTeamId**: You can check your apple team ID [here](https://developer.apple.com/account/) (e.g., `12LE34XI45`). + +## Build your App for iOS + +Before publishing, you'll need to build your app by instructing Expo to generate an iOS build. + +It is recommended to build your app with the `preview` profile before releasing to verify that it works as expected. See [this tutorial](tutorial/building) to learn more about build profiles. + +Run this command: + +```bash +eas build --platform ios --profile preview +``` + +When you run the above command, Expo will prompt you for your Apple ID and password. + +Once the above step has been completed, login to your account on [Expo](https://expo.dev) and download your newly built app. + +Navigate to your project in the [Expo web console](https://expo.dev), then click on the **Builds** menu located on the left-hand side of the screen. + +- Click on the project you want to install. + Builds + +- Download the iOS build by pressing the `Download` button in the `Build Artifact` section. + Build Artifact + +This will download a tar file containing your app. Extract the file, then drag it to your simulator to install it. See [this section](tutorial/building#1-preview) of the tutorial to learn about running the app on real devices. + +Once you have verified that the app runs as expected, you can proceed to build it for release: + +```bash +eas build --platform ios --profile production +``` + +The approach for a production build is similar to the one used for generating a preview build. However, unlike a preview build, you won't be able to launch the production build in the iOS simulator—it is intended solely for publishing to the App Store. + +Once this process is completed, you can proceed with submitting it to Apple. This process typically involves Apple's TestFlight service. + +## Submit to TestFlight + +TestFlight is a key aspect of Apple's Developer Program, which enables developers to provide beta users with access to their app under less restrictive review requirements. + +With TestFlight, you're able to invite users to test your app and collect their feedback before releasing it to the public on the App Store. You can learn more about TestFlight [here](https://developer.apple.com/testflight/). + +Submitting an iOS app is much easier with EAS Submit. This is covered in more detail in the [tutorial](tutorial/publishing). + +Run the following command to start publishing the app to TestFlight: + +```bash +eas submit --platform ios --profile production +``` + +Once the process has completed successfully, we can check the build in App Store Connect. + +In App Store Connect, click on the TestFlight Tab. + +You'll see the [status](https://help.apple.com/app-store-connect/#/dev3d6869aff) of your built version. + +- **Red** indicates that you need to perform some action. +- **Yellow** indicates that some aspect of the process is pending—either from you, or from Apple. +- **Green** indicates that the build is being tested in TestFlight, or is ready to be submitted for review. + +You won't be able to begin beta testing with TestFlight until an official tester from Apple verifies your app. + +In order to allow Apple to properly test your Lexicon-powered app, they'll need to have credentials to login your Discourse site. + +Before submitting your app, you'll need to create those credentials in Discourse and specify them in App Store Connect. + +- In App Store Connect, click on your app. +- Click on TestFlight App. +- Click on Test Information in the sidebar on the left-hand side. +- Fill the required fields, then check the `Sign in required` checkbox, and enter the credentials. + Review Information Sign In +- Please also provide information for a person to contact if the review team needs additional information. + Review Information Contact + +### Specify Users for Beta Testing + +Beta Test Users can belong to an Internal Group or an External Group. + +You can specify internal users by going to the Internal Group section, and clicking on **App Store Connect Users**. + +Similarly, you can specify external users by selecting External Groups, and clicking on **Add External Testers**. + +#### More Information + +TestFlight and App Store Connect are sophisticated tools to help with the process of submitting, testing, and publishing your app. + +If you have further questions or just want to learn more, we'd recommend that you make use of Apple's documentation, which is very high quality. + +For more information about TestFlight in general, read the [documentation](https://developer.apple.com/testflight/). + +Similarly, for specific information about beta testing with TestFlight, check out [Testing Apps with TestFlight](https://testflight.apple.com/). + +## Publish to the App Store + +Once you've successfully passed Apple's review process and have received enough feedback from your beta testers, you're ready to publish to the App Store and go live! :tada: + +As a few final reminders, double-check that... + +- Your Discourse instance is online, reachable, and functioning correctly. +- The built version of your app is configured to point at the correct Prose server. +- Your Prose server is online, reachable and healthy. +- Your Prose server is deployed with the [recommended guidlines](dedicated#configure--deploy-prose) for production. + - In particular, ensure that its traffic is encrypted using an SSL certificate. + +Next, we'll guide you through the process of publishing your app for Android devices on the Google Play Store. diff --git a/documentation/versioned_docs/version-2.1.0/assets.md b/documentation/versioned_docs/version-2.1.0/assets.md new file mode 100644 index 00000000..a24dcad0 --- /dev/null +++ b/documentation/versioned_docs/version-2.1.0/assets.md @@ -0,0 +1,47 @@ +--- +title: App Icon & Other Assets +--- + +The Lexicon Mobile App contains multiple assets that can be replaced in order to White Label it. + +The assets that can be modified are as follows: + +## App Logo + +Used to show the app logo in the application, such as on the Login, Register, and 2FA Scenes. + +The assets are located at `frontend/assets/images/logo.png` and `frontend/assets/images/logoDark.png`. The `logo.png` is used in light color scheme and `logoDark.png` is used in dark color scheme. To customize it, simply replace the existing file with your own `logo.png` and `logoDark.png`. + +## Favicon + +Used to show the app logo. + +The asset is located at `frontend/assets/favicon.png`. To customize it, simply replace the existing file with your own `favicon.png`. + +## Image Placeholder + +Used to temporarily take an image place when it is loading. + +The asset is located at `frontend/assets/images/imagePlaceholder.png`. To customize it, simply replace the existing file with your own `imagePlaceholder.png`. + +## Icons + +Used to display icons inside the application. + +The assets are located in the `frontend/assets/icons` folder. If you want to add more or edit the remaining icons, you need to insert the icons to the `frontend/assets/icons/` folder and import them in `frontend/src/icons.ts`. + +There are some standards applied to the icons, such as: + +#### Uniform Icon Size to Maintain Visual Consistency + +The UI is designed around the default base dimensions of 28x28px for icons. + +If you adjust this, you may need to modify other aspects of the theme or fonts in order to maintain a clean appearance. + +Similarly, if you provide a new icon that does not conform to these dimensions, you may run into visual inconsistencies. + +#### SVG Icons have their Fill Color Controlled via `currentColor` + +If you are adding a new icon that you expect to interact with theme's colors, ensure that its color is not hard-coded, and is instead set to `currentColor`. + +If you are unfamiliar with this concept, take a look at the [MDN Specification on SVG color values](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/color). diff --git a/documentation/versioned_docs/version-2.1.0/commercial-support.md b/documentation/versioned_docs/version-2.1.0/commercial-support.md new file mode 100644 index 00000000..9d164810 --- /dev/null +++ b/documentation/versioned_docs/version-2.1.0/commercial-support.md @@ -0,0 +1,9 @@ +--- +title: Commercial Support +--- + +With official support, you get expert help straight from the core team. We provide app customization, dedicated support, prioritize feature requests, deployment strategies, advice on best practices, design decisions, and team augmentation. + +Additionally, we are open to engagements for non-technical site owners looking to customize, deploy, and launch a mobile app for their Discourse users. + +Reach out to us for consulting at support@kodefox.com. diff --git a/documentation/versioned_docs/version-2.1.0/concepts.md b/documentation/versioned_docs/version-2.1.0/concepts.md new file mode 100644 index 00000000..44211e43 --- /dev/null +++ b/documentation/versioned_docs/version-2.1.0/concepts.md @@ -0,0 +1,68 @@ +--- +title: Concepts and Architecture +--- + +import useBaseUrl from '@docusaurus/useBaseUrl'; + +## Prose: Discourse through GraphQL + +It is worth acknowledging upfront that Discourse already provides a traditional, RESTful API for developers out of the box. + +However, [the official documentation](https://docs.discourse.org/) for this API points out that it is incomplete, effectively serving as a starting point. + +> Note: For any endpoints not listed you can follow the reverse engineer the Discourse API guide to figure out how to use an API endpoint. +> +> —**Discourse API Documentation** + +The core team, as well as members of the [support forum](https://meta.discourse.org), regularly respond to questions about the API by [encouraging developers to reverse-engineer the API](https://meta.discourse.org/t/how-to-reverse-engineer-the-discourse-api/20576). As of this writing, the topic for how to reverse engineer the API has been linked to from nearly 200 other topics on the support forum. + +To help you simplify the process for you, Prose strives to normalize a subset of the API. We have done so with the hope that it will save you some time as you develop against Discourse. + +#### GraphiQL + +Prose's GraphQL implementation includes an [in-browser GraphQL IDE](https://www.graphql-yoga.com/docs/features/graphiql), known as [GraphiQL](https://github.com/graphql/graphiql), which allows developers to easily reference the entire documentation and schema and make queries against a running Discourse instance. + + + +This means you can rapidly get a clear understanding of how a method behaves—and what parameters it requires—without digging through support posts or reverse-engineering the REST API. + +#### Why GraphQL? + +There is no shortage of articles about both the [benefits](https://www.howtographql.com/basics/1-graphql-is-the-better-rest) and [tradeoffs](https://lwhorton.github.io/2019/08/24/graphql-tradeoffs.html) of GraphQL. + +We're well aware that GraphQL isn't some magical solution that solves all the problems of other API paradigms. + +Having said that, we chose to build Lexicon with it for two primary reasons. + +1. Our team is familiar and fluent with GraphQL, and deeply enjoys working with it. + +2. The tooling, libraries, and auto-generated documentation provide out-of-the box benefits which we can pass onto others with no additional effort. + +#### Why Expo? + +[Expo](https://docs.expo.io/) is both a framework and a platform for building universal React applications. In particular, it provides a superior development experience when building mobile apps with React Native. + +We find that Expo makes us much more effective as developers, and also provides excellent services to facilitate the entire process of building and publishing React Native apps. + +In particular, Discourse sites that leverage the [Lexicon Discourse Plugin](./discourse-plugin.md) get the benefit of [push notifications](./push-notifications) through Expo's [push notifications service](https://docs.expo.dev/push-notifications/overview/), which abstracts away Google and Apple's push services into a simple interface. + +## Lexicon Architecture + +The Lexicon Stack is fairly simple, and only consists of 3 major pieces: + +- The Lexicon Mobile App +- The Prose GraphQL API +- A running, accessible Discourse instance +- Optionally, you can install our [Discourse Plugin](./discourse-plugin.md) to enable additional features. + +Below is a diagram illustrating the typical architecture for a Lexicon-powered mobile app. + +IOS Lexicon Login Page + +As indicated above, the mobile app makes requests to a deployed Prose GraphQL server. + +The Prose server has been configured to point at an active Discourse instance of the developer's choice. + +If the [Lexicon Discourse Plugin](./discourse-plugin.md) is installed, additional endpoints will be exposed which Prose already knows how to communicate with. + +Traffic then flows back from Discourse, through Prose, and returns to the mobile app over a GraphQL interface. diff --git a/documentation/versioned_docs/version-2.1.0/contributing.md b/documentation/versioned_docs/version-2.1.0/contributing.md new file mode 100644 index 00000000..0fe6fbe9 --- /dev/null +++ b/documentation/versioned_docs/version-2.1.0/contributing.md @@ -0,0 +1,131 @@ +--- +title: Contributing +--- + +Thank you for your interest in contributing! :sparkles: + +We greatly appreciate the time and effort you're willing to put forth to make Lexicon even better. + +There are several ways to help out. + +## Reporting Bugs + +The best way to let us know about a bug is by [creating a new issue](https://github.com/lexiconhq/lexicon/issues/new) on [Github](https://github.com/lexiconhq/lexicon). + +As always, we recommend searching the existing open and closed issues before opening a new one. + +When you create the issue, please be sure to include the following: + +- A detailed description of the bug and its behavior + +- The behavior you expected instead of the bug + +- A list of steps for how to reproduce the bug + +- Details about the device(s) and version(s) you're observing the bug on + +- Screenshots and screen recordings, while not necessary, are very welcome! + +Once we've received your bug report, we will triage it and label it accordingly. + +## Contribute to the Project + +Want the honor of being listed in our contributors section :clap:? + +We'd love to get a PR from you addressing an existing issue, adding a feature, or even just improving the documentation. + +To get started contributing, follow the instructions below. + +### Instructions + +**1. Fork the [official Lexicon repository](https://github.com/lexiconhq/lexicon)** + +You probably already know the drill - click on **Fork** button on the upper-right corner. + +**2. Clone your Fork of Lexicon** + +Be sure to clone **_your_** fork to your development machine (as opposed to cloning the main Lexicon repository). + +``` +$ git clone https://github.com/YOUR_USERNAME/lexicon.git +``` + +If you need further guidance with cloning, head over to our [Quick Start](quick-start#installation) section. + +Just bear in mind that the Quick Start section walks you through cloning the Lexicon repository. So make sure you change the URL to your username as referenced above. + +**3. Run and connect the app with Prose and a Discourse Host** + +For a comprehensive walk-through of this step, follow the instructions in the [**Setup**](setup#discourse-host) section. + +If you already have a deployed Prose instance that is pointing at a Discourse instance, you can simply configure the Lexicon Mobile App to point at the address of your Prose deployment. + +However, if you don't have that, or if you're planning on making adjustments to the Prose server itself, you'll want to ensure the Lexicon Mobile App is configured to point at a Prose server that you have running locally. + +**4. Get Started with your Contribution** + +At this point, you should be setup to dig in on the main work of your feature, bugfix, or other contribution. + +Remember that it's necessary to have the [**ESLint**](https://eslint.org/docs/user-guide/getting-started) and [**Prettier**](https://prettier.io/) plugins installed in your IDE, as those are required in order for the Pull Request checks to pass. + +We would recommend working in [VSCode](https://code.visualstudio.com/), since that is what we used to develop Lexicon. However, it is up to you, you only need to ensure that ESLint and Prettier are functioning properly within your IDE. + +**5. Run the Test Suite** + +Follow these [**steps**](setup#run-the-test-suite) to run the Lexicon test suite. + +In order to speed up the feedback cycle, it is recommended that you ensure that all tests are passing locally before pushing, especially if you already have an open PR. + +This is primarily because we have configured our Github project to block PRs from being merged if any of the build steps fail. + +If the reviewers see that tests are failing, they aren't able to review it as quickly, and will likely request that you resolve any build issues before requesting review again. + +**6. Stage, Commit, and Push your Local Changes** + +If you're unfamiliar with this process, please take a look at this [great article](https://github.com/git-guides/#learning--mastering-git-commands) from Github to bring you up to speed. + +**7. Create a New Pull Request** + +Your code is ready to submit! :tada: + +Go to the Lexicon [Pull Requests tab](https://github.com/lexiconhq/lexicon/pulls), and compare the changes between your branch and the master branch. + +Double-check and make sure you didn't push anything you don't want included in your PR. + +Then, go ahead and create a new Pull Request from your forked repository. + +Please be sure to follow the Pull Request template, add related labels, and please mention the issue you are addressing to help us keep track of what's being worked on. + +## Share Your Thoughts with Us + +We'd love to hear your new ideas! Drop them in the [Issues tab](https://github.com/lexiconhq/lexicon/issues). + +## Spread the Word + +Let others know about your awesome experience using Lexicon on social media, and tag us on Twitter [@GetLexicon](https://twitter.com/GetLexicon). + +And if you build your app using Lexicon, please let us know. We'd love to help you spread the word about what you've built! + +## Improve the Documentation + +As a closing thought, if you find any issues with the Lexicon documentation, or just think you could make it better, you can get started with these brief instructions below. + +To generate and run the documentation locally, from the project root, run: + +```sh +npm run docs:start +``` + +Similarly, you can build the documentation using: + +```sh +npm run docs:build +``` + +All documentation is in the `documentation/` directory, and the Markdown pages used to generate this site are under `documentation/docs`. + +If you end up making a PR to improve the documentation, please be sure to label your PR with the `Documentation` label. + +:::note +Don't hesitate to ask if you have any further questions. We're always happy to help. :smile: +::: diff --git a/documentation/versioned_docs/version-2.1.0/customize.md b/documentation/versioned_docs/version-2.1.0/customize.md new file mode 100644 index 00000000..6797d756 --- /dev/null +++ b/documentation/versioned_docs/version-2.1.0/customize.md @@ -0,0 +1,28 @@ +--- +title: Customization +--- + +## Theming + +As part of its [White Labeling Support](white-labeling), Lexicon allows you to customize the theme of the mobile app. +You can configure the base and functional colors according to a color scheme of your choosing. +You're also able to customize icons, fonts, and even the error messages that appear inside of the mobile app. +To get started with this, check out the [Theming page](theming) under the [White Labeling](white-labeling) section. + +## White Labeling the Mobile App Assets + +To provide your users with a unique experience that matches your brand, you can customize the splash screen and app icon on their device. + +This will replace all Lexicon branding with your own. + +Further details can be found in both the [Tutorial](tutorial/white-label) and the [White Labeling Section](white-labeling) of this documentation. + +## Enabling Additional Discourse features + +As you might already be aware, Discourse is a highly customizable piece of software. Much of it is customizable from the Admin Site Settings page on your Discourse instance. + +Some of these settings will translate automatically into the Lexicon Mobile App, such as `authorized extensions`. + +In general, we have done our best to get out of the way and use Discourse as the source of truth for how the Lexicon Mobile App should appear and behave. + +If you find any settings that Lexicon is not responding to, but you feel it should, please open an issue and let us know. diff --git a/documentation/versioned_docs/version-2.1.0/dedicated.md b/documentation/versioned_docs/version-2.1.0/dedicated.md new file mode 100644 index 00000000..75fe8afe --- /dev/null +++ b/documentation/versioned_docs/version-2.1.0/dedicated.md @@ -0,0 +1,300 @@ +--- +title: Hosting & Configuration +--- + +As mentioned in the [Overview](deployment), this section is meant to guide you through configuring and deploying Prose on a dedicated instance. + +## Decide on Where to Host + +First, you'll need to answer the following question. Where would you like to host Prose? + +While there are many options that vary by project and developer preferences, the simplest way is often to use a cloud provider of your choice. + +In the [Lexicon tutorial](tutorial/setup-cloud-server), we walk you through this process using Digital Ocean. + +If you're confused about this step, or don't have a preference, you should take some time to work through it. + +However, if you already know what you're doing, feel free to use any cloud provider or hosting solution of your choice. + +### Hosting Checklist + +Once you've decided on a host, go through the checklist below to verify that everything is setup as expected. + +#### ✅ Ensure Access & Permissions on the Host + +At a minimum, you will need to be able to login to the host. Some cloud providers offer a virtual, web-based terminal, but ideally you can get credentials to login directly. + +If your host is in a UNIX-based environment, you should also have permissions to run commands as `sudo`. + +A quick way to check this is to simply attempt to run a command with `sudo`: + +```sh +$ sudo ls +``` + +However, if you have a restrictive hosting environment, you will just need a way to place the Lexicon source onto the host, install its dependencies, and expose it on a port. + +Bear in mind that a restrictive hosting environment is not ideal, especially since the recommended setup makes use of Docker. + +#### ✅ Ensure the Host is reachable in the way you need it + +Typically, this means that your host is accessible on the open internet. + +However, you might have different constraints, such as only needing the host to be accessible from within a VPN or a local network. + +
+ +Once you have setup a host which is reachable in the way you need it to be, you can begin configuring Prose on it. + +## Configure & Deploy Prose + +### Without Docker + +Naturally, setting up Prose without Docker involves more manual steps and can be platform-specific. + +We have already covered this approach well in the tutorial. In particular, you can dig in with it on the page, [Setup the Prose GraphQL API](tutorial/install-prose#install-manually) + +### With Docker + +The Prose Docker image comes preconfigured to run Prose using **[PM2](https://pm2.keymetrics.io/)**, which is a sophisticated toolset for running Node processes in production. + +This is typically a reasonable setup, with which you can even expose the PM2 server directly to requests on the host. + +However, if you'd prefer a different setup, perhaps using Nginx as a reverse proxy to the Docker container, feel free to modify the Dockerfile to match your requirements. + +#### Install Docker + +**[Docker](https://www.docker.com/)** is a containerization framework that makes it easy to build, manage, and deploy your application stack in a way that is safer, more reliable, and reproducible across multiple platforms. + +There are countless guides available for installing Docker on a given operating system. + +Ubuntu is one of the more common operating systems avaiable through most cloud providers. + +Docker provides a [full tutorial](https://docs.docker.com/engine/install/ubuntu/) for this, and even provides a convenience script that you can run in two lines: + +```sh +curl -fsSL https://get.docker.com -o get-docker.sh +sudo sh get-docker.sh +``` + +Whichever path you need to take, just make sure that Docker is up and running on your host before continuing. + +#### Configure Environment Variables + +A comprehensive list of all Prose environment variables can be found on the [Environment Variables](env-prose) page. + +In brief, at a minimum, you'll want to ensure that `PROSE_DISCOURSE_HOST` is set. + +Another variable to pay attention to is `PROSE_APP_PORT`. This defaults to port 80, which instructs Prose to listen on that port. + +Depending on your setup, you might want it to listen on a different port. + +
+ +#### Build Prose from the Dockerfile + +If you'd like to use Docker to manually build Prose, run the following command from the **project root**. + +This might be of interest to you if you'd like to make some adjustments to the Dockerfile itself. + +Alternatively, if you simply wish to pull the latest Prose build from Docker Hub, you can [skip to the next step](#pulling-the-prose-docker-image). + +Unless you've made modifications to the Dockerfile and have it stored elsewhere, you can get started building by running: + +```bash +docker build -t prose:latest -f api/deploy/Dockerfile api/ +``` + +The command searches for the `Dockerfile` at `api/deploy/Dockerfile` because we instructed it to look there with the `-f` flag. + +Then, it uses `api/` as the context for the build, which allows the references in the `Dockerfile` to resolve correctly. + +By passing the `-t prose:latest` tag, it tags the locally built image as the latest build. This can be useful for identifying and managing the images in a Docker environment over time. + +#### Pull the Prose Docker Image + +If you'd rather just use the latest release of the Prose image, you can simply run: + +``` +docker pull kodefox/prose:latest +``` + +#### Run the Prose Docker Container + +Next, to run the newly built image, run the following command: + +```bash +docker run -d \ + -e PROSE_DISCOURSE_HOST=https://discourse.example.com \ + -e PROSE_APP_PORT=4000 \ + --name prose \ + -p 5000:4000 \ + kodefox/prose:latest +``` + +:::note +If you built the image by hand, you'll want to substitute `kodefox/prose:latest` with the image name and tag you used, such as `prose:latest`. +::: + +To recap, let's briefly break down that command line-by-line + +**Run in Detached Mode** + +```bash +docker run -d +``` + +The first line lets Docker know to run the container in **detached mode**. + +This means that the command will run in the background, will not be tied to your current session, and will keep running even if you log out. + +If you omitted the `-d` flag, Docker would run the container in the foreground, and exiting the process in the foreground would stop the container. + +**Set Environment Variables** + +```bash +-e PROSE_DISCOURSE_HOST=https://discourse.example.com +-e PROSE_APP_PORT=4000 +``` + +These lines instruct Docker to pass the environment variables of `PROSE_DISCOURSE_HOST` and `PROSE_APP_PORT` to the container when running it. + +These are both application-level environment variables that Prose itself will leverage to run properly. + +The Docker image expects these values to be set and passes them to the container's environment, which Prose then accesses via `process.env`. + +**Name the Container** + +```bash +--name prose +``` + +This line tells Docker to give the running container a name. This makes it easier to identify and interact with via commands, such as: + +```bash +docker stop prose +``` + +**Configure a Port Mapping between the Host and the Container** + +```bash +-p 5000:4000 +``` + +Next, we configure Docker with a port mapping, which tells Docker to listen to map the host port of `5000` to the container port of `4000`. + +Because we previously set `PROSE_APP_PORT=4000`, this means that all requests to the host at port `5000` will be forwarded to Prose inside of its container on port `4000`. + +```bash +kodefox/prose:latest +``` + +The last line of the command tells Docker which image to use for the container. + +Above, if you built the Prose image by hand, it was tagged as `prose:latest`. + +If you chose to pull from Docker Hub, this is simply instructing Docker to pull that image if necessary, and then start the container with it. + +#### Next Steps + +At this point, you should have a Docker container running the Prose server on your host. + +However, in terms of preparing your Prose host for production, you aren't quite there yet. + +Below, we'll guide you through the last steps, finalizing your deployment of the Prose GraphQL Server. + +#### Setup SSL (IMPORTANT) + +:::danger +Deploying Prose without SSL in a way that is publicly accessible is **extremely risky**. + +Doing so could provide an attacker with full access to your Discourse site and all of its data. +::: + +The **most important next-step** to take at this point is to configure an SSL certificate for your Prose host. + +The reason this is so important is that, without SSL, Prose's traffic between your users' devices and Discourse is not encrypted. + +And this means that attackers can snoop on your users' requests to Prose and Discourse—including, importantly, their authentication information. + +To put it bluntly, deploying Prose without configuring SSL is irresponsible and compromises the security of your Discourse instance. + +An attacker could even steal your authentication token and use it to access, and potentially destroy, your Discourse site. + +##### How to Setup SSL + +There are a variety of methods to obtain SSL certificates. Some are free, and some are paid. + +The free route involves using [Let's Encrypt](https://letsencrypt.org/), which is very useful, but can require more technical knowledge to setup correctly—depending on your configuration. A key difference is that you need to renew the certificates more frequently. + +The paid route involves using a provider like [DigiCert](https://www.digicert.com/) to obtain certificates that take longer to expire. + +Either way, you'll end up with certificate files that you can configure and launch your webserver with. + +Ideally, at this point, you've already purchased a domain. If you haven't, we'd recommend using a domain provider to get a low-cost domain name. + +You could host Prose at a subdomain of your existing Discourse site, like `prose.mydiscoursesite.com`. + +Or, you could just get a cheap, nonsense domain, like `purplemonkeydishwasher.tech`—since your users won't typically see it anyway. + +Regardless, to emphasize it again, it is **critical** that you don't deploy Prose into production until you have prepared your host to encrypt the traffic from Prose. + +#### Determine how you'll expose Prose on the host + +When someone navigates to your host which is running Prose, how will their request get routed to Prose? + +If you had exposed Prose directly on port 80—NOT recommended—and your host's domain name was `myproseserver.com`, then a user would navigate to `http://myproseserver.com` and be greeted with the [GraphiQL interface](https://www.graphql-yoga.com/docs/features/graphiql). + +However, a more common approach is to use a dedicated webserver, such as Nginx or Apache, that acts as a reverse-proxy. + +With this approach, the websever listens for all requests on the ports you tell it to, and is configured to route traffic to Prose, which is listening on a non-privileged port, like 8080. + +We recommend this approach more highly for the following reasons: + +- Existing webservers are generally more reliable and performant +- It allows configuration of an SSL certificate, which is necessary for protecting your users' data + +Upon configuring the webserver, you'll need to instruct it to forward traffic to the running Prose server. + +Your setup might look something like this: + +- Nginx is configured to listen on port 80 and port 443 on your domain, `purplemonkeydishwasher.tech` +- Nginx has located and loaded your SSL certificate files for `purplemonkeydishwasher.tech` +- Nginx is configured to upgrade all requests on port 80 to port 443 +- Your Prose server is running inside of Docker on a container port of 80, and exposed to the host on port 8080. +- Your Nginx configuration specifies that requests to `purplemonkeydishwasher.tech` should be forwarded to port 8080. +- Requests come in for `purplemonkeydishwasher.tech`, Nginx routes it to the container running Prose, which handles the requests, and responds. + +#### Configure your Cloud Provider's Firewall, if one exists + +Ideally, you've configured Prose to be exposed on the open internet with the traffic encrypted over port 443. + +Depending on your cloud provider, you may need to go into its settings and expose that port on the firewall. + +For example, in DigitalOcean, this involves going to the Networking section, and creating a new firewall rule. + +From there, it is fairly simple to add common ports, like 80 and 443, to the firewall. + +After that, you simply apply the firewall to your particular instance, and traffic should be allowed through. + +#### Configure DNS Settings for your Domain + +Provided that you've already registered a domain name, you'll need to configure it so that the domain name points to your host which is running Prose. + +Depending on your setup, this will either be done in your domain provider's settings panel, or perhaps within your cloud provider. + +Continuing with the DigitalOcean example from above, you can configure your domain provider to point at DigitalOcean's name servers. + +This effectively tells your domain provider that DigitalOcean will handle everything for you, and allows you to make adjustments to your domain from within DigitalOcean. + +In that case, DigitalOcean makes it seamless to map the domain name to your instance's IP address, and it should then be accessible from the domain name. + +Otherwise, you'll want to get the IP address of your host, go into your domain provider, and instruct it that requests to your domain should be direct to your host's IP address. + +#### Ready to Go + +At this point, your deployed host should be running Prose correctly. When you navigate to the domain name that you configured it with, you should see [GraphiQL](https://www.graphql-yoga.com/docs/features/graphiql), which will allow you to make GraphQL queries against your Discourse instance. + +We understand that the details of your deployment can vary quite a bit depending on how you chose to do it. + +If you run into any issues with this step—as always—don't hesitate to reach out to us for support. diff --git a/documentation/versioned_docs/version-2.1.0/deployment.md b/documentation/versioned_docs/version-2.1.0/deployment.md new file mode 100644 index 00000000..b46ab73b --- /dev/null +++ b/documentation/versioned_docs/version-2.1.0/deployment.md @@ -0,0 +1,86 @@ +--- +title: Overview +--- + +As covered in [Concepts and Architecture](concepts#prose-discourse-through-graphql), Prose is Lexicon's GraphQL API layer on top of the traditional RESTful API provided by Discourse. + +## Getting Started with Deployment + +At this point, you're likely to be digging into this section of the documentation for two reasons: + +- You've been developing against a local instance (or container) running Prose, and you're ready to actually deploy your entire Lexicon project to production. + +- You want to simplify your development process by pointing the Lexicon Mobile App at a deployed instance of Prose. + +In either scenario, the end goal of this section is to have a working Prose server accessible on the open internet. + +### 🔐 Note about Access Control + +As a brief aside, please note that Prose cannot expose any information from Discourse that Discourse is not already exposing on its own. + +If your Discourse instance requires authentication, then Prose will be unable to retrieve most queries unless the required authentication information is provided by the user accessing Prose. + +### 🧱 Alternative Deployment Strategies + +Initially, we wanted to provide instructions for an integrated deployment strategy. This would have involved deploying Prose on the same host as your Discourse instance, and ideally finding a way to deploy and expose it within the running Docker host that Discourse uses itself. + +This is still achieveable. But for now, we have opted to focus solely on deploying Prose as a dedicated instance. + +However, should you find yourself preferring a custom deployment of Prose, we would encourage you to do so. + +If you do, and you have some questions or challenges you're encountering, please reach out to us. + +Ideally we can help you sort things out and work your approach into our documentation so that everyone will benefit going forward. + +## Deploying as Dedicated Instance + +As mentioned above, the official deployment strategy for Prose is to host it as a dedicated instance. + +Like anything, this comes with both benefits and trade-offs, which we have outlined for you below. + +### 🚀 Benefits + +A dedicated host for Prose will have better performance and reliability because its only resource usage comes from running Prose. i.e., it has exclusive usage of CPU, RAM, disk space, etc. + +If, on the other hand, you had managed to deploy Prose on the same host as your Discourse instance, this would mean that both Prose and Discourse need to share the host's allocated resources. If your Discourse instance is already running on a fairly light host, running Prose on it might mean that you would need to upgrade to a host with more resources. + +### ⚠️ Possible Trade-offs + +#### Increased Cost + +Naturally, if you're setting up a dedicated host to run Prose, then that involves additional costs on top of what you're already paying to host Discourse. + +Having said that, for most deployments, it is unlikely that you will need to allocate an expensive amount of resources to Prose. + +For example, on Digital Ocean, the $5 Shared CPU node is often sufficient. + +#### Potential for Increased Latency + +By nature, when deploying Prose on a different host from your running Discourse instance, the latency between the mobile app and Discourse increases. + +This is because each request has to make two hops: + +- The first request is from the client (your Lexicon-powered mobile app) to the Prose GraphQL API +- The second request is from Prose to Discourse + +However, the only important questions regarding this point are: + +- How much measurable latency is there? +- Is it noticeably slow to myself or my users? + +This, of course, can depend on several factors: + +- Where your Discourse server is deployed +- Where your Prose server is deployed +- Where your users tend to be +- If the amount of traffic (load) is too much for the system to optimally run both Prose and Discourse. + +If you are observing noticeable latency, we would recommend looking into these factors. + +Ideally, you'll want to deploy Prose in the same region as your Discourse instance; and it is even better if you can deploy Prose in the same datacenter as your Discourse instance. + +## Up Next + +With this overview out of the way, we'll start by introducing you to the list of all possible [environment variables](env-prose) that may be necessary or useful when deploying Prose. + +Lastly, we'll get into the heart of it, by [preparing your host and deploying Prose](dedicated). diff --git a/documentation/versioned_docs/version-2.1.0/discourse-features.md b/documentation/versioned_docs/version-2.1.0/discourse-features.md new file mode 100644 index 00000000..f073c3c4 --- /dev/null +++ b/documentation/versioned_docs/version-2.1.0/discourse-features.md @@ -0,0 +1,53 @@ +--- +title: Discourse Features Support +--- + +Below is a table of Discourse features which provides the details and current status about the support for a given feature in the **Lexicon Mobile App**. + +If we missed one, or anything looks out of date here, don't hesitate to submit a Pull Request which updates the table. + +Is the feature you love not supported? [Reach out to us](mailto:support@kodefox.com) to discuss how we can bring it to life for you. + +#### Our General Approach to Feature Support + +Much of our initial focus was on using-facing features, rather than administrative features. + +This is why, for example, users can select categories for their topics, but administrators are unable to create new categories from within the mobile app. + +For this reason, most admin tasks are still best accomplished using the Discourse web app on a larger device. + +### Lexicon Mobile App Features + +| Feature | Description | Supported | Notes | +| ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------- | --------- | --------------------------------------------------------------------------------------------------------------------- | +| 2FA Login | Allow users with 2FA enabled to be prompted for their 2FA code when logging in | ✅ | Managing 2FA, such as enabling it or disabling it from within the app, is not currently supported | +| Ability to Tag Topics | Create and tag topics to provide relevant metadata for your users | ✅ 🔧 | Configuration required: see [Optimal Experience](optimal#enable-topic-tagging) | +| Topic Previews (Excerpts) | Show an excerpt of the first post in a topic from the Home screen | ✅ 🔧 | Configuration required: see [Optimal Experience](optimal#enable-topic-excerpts) | +| View User Activity | View a user's recent activity—such as topics, posts, and likes—in a single feed from their profile | ✅ | The ability to filter by activity is not currently supported | +| Topic Metrics | Likes, Views, Replies, and Frequent Posters | ✅ | | +| Topic & Post Actions | Ability to like and edit topics and posts | ✅ | | +| View Top & Latest Topics | A Tab View at the top of the main feed provides the ability to switch between Latest and Top activity | ✅ | | +| Search | Search the current Discourse instance for topics and posts based on keywords, categories, and tags | ✅ | | +| Categories | View the category of a topic and filter topics by a given category | ✅ | Categories cannot be created, updated, or deleted | +| Attaching Media to Posts | Users can attach media to a post from the app | ✅ 🔧 | Configuration recommended for supported file extensions-see [Optimal Experience](optimal#configure-upload-extensions) | +| Standard Markdown | Standard Markdown is supported in the editor and rendered correctly in the mobile app | ✅ | Light, incomplete support exists for some of Discourse's custom markup, such as dates | +| Sign Up | Allow users to sign up for an account directly through the mobile app, depending on whether your Discourse instance allows new user registration or not | ✅ | | +| Browsing Public Instances | Allow users to immediately access and browse your Discourse instance from the mobile app if it is not private | ✅ | Users will be prompted to login upon attempting an authenticated action | +| User Profiles | Ability to view users' profiles and edit your own | ✅ | Partial support: displays the user's photo, username, Markdown bio on a single line, and recent activity | +| Post Flagging | Allow users to flag posts for admins to review | ✅ | Admins are not able to review posts in the app, though they will see in-app notifications for flags | +| Mark Discourse Notifications Read | Allow users to see new notifications from the profile screen of the mobile app and mark all notifications as read | ✅ | Some notifications from Discourse are not tappable in the mobile app, such as badge notifications | +| Private messaging | Allow users to start private or group messages with one another | ✅ | | +| Mentions | Allow users to mention a user when creating or editing posts and messages | ✅ | +| Color Scheme | Provides light and dark mode support for users | ✅ | Specify color scheme (light mode, dark mode, or system) from within the app (only local to the user's mobile device) | +| Discourse Emojis | Utilize emojis when creating a topic, making a post, or sending a reply | ✅ | Discourse BB Code emojis and Unicode-based emojis are fully supported. | +| User Status | Allow users to update their statuses and view the statuses of other users | ✅ | | +| Polls | Allow users to create polls with custom settings in posts and private messages. Enable users to view and vote on the polls. | ✅ | | +| Badges | The ability to see and interact with badges that have been awarded to users on the Discourse instance | ❌ | | +| Post Drafts | Enable users to start composing a draft of a post and return to it later | ❌ | | +| Groups | Enable users to create and participate in private groups of which only group members can view certain topics | ❌ | | +| Admin Features | Discourse admin features generally not available in Lexicon—better suited to a desktop environment | ❌ | Editing posts is supported | +| Post Quotes, Toggles, and Task Lists | Custom text formatting that enables Discourse-specific features | ❌ | | +| Post Bookmarks | Allow users to bookmark certain posts or topics | ❌ | | +| DiscourseConnect (SSO) | Replace Discourse authentication with a Custom Provider | ❌ | | +| Custom Authentication Plugins | Login via OAuth2 or other protocols using custom Discourse Plugins | ❌ | | +| Real-time Chat | Enable users to initiate conversations using the chat feature, either in a channel or through private messaging | ❌ | | diff --git a/documentation/versioned_docs/version-2.1.0/discourse-plugin-enable.md b/documentation/versioned_docs/version-2.1.0/discourse-plugin-enable.md new file mode 100644 index 00000000..c4b5ff23 --- /dev/null +++ b/documentation/versioned_docs/version-2.1.0/discourse-plugin-enable.md @@ -0,0 +1,34 @@ +--- +title: Enable the Lexicon Plugin +slug: discourse-plugin/enable +--- + +import useBaseUrl from '@docusaurus/useBaseUrl'; + +--- + +After you have confirmed the plugin has been installed and your Discourse instance is running again, you can follow these steps to enable the plugin: + +1. As an admin user, access your Discourse admin dashboard. + +2. Navigate to the `Plugins` tab. + +You'll notice that the `discourse-lexicon-plugin` is not enabled yet. + +Plugin Admin Page + +3. Click on the `Settings` button for the `discourse-lexicon-plugin` entry. + +4. Select the feature you want to enable and turn it on. + +##### Push Notifications + +For push notifications, all you need to do is check the box for `lexicon push notifications enabled`. This is covered in [Enable Push Notifications](./push-notifications/setup/enable-push-notifications.md). + +##### Email Deep Linking + +For email deep linking, you need to fill in your app scheme first before enabling it. + +Plugin Settings Page + +This is covered in detail in [Enable Email Deep Linking](./email-deep-linking/setup/enable-email-deep-linking.md). diff --git a/documentation/versioned_docs/version-2.1.0/discourse-plugin-installation.md b/documentation/versioned_docs/version-2.1.0/discourse-plugin-installation.md new file mode 100644 index 00000000..911770b1 --- /dev/null +++ b/documentation/versioned_docs/version-2.1.0/discourse-plugin-installation.md @@ -0,0 +1,82 @@ +--- +title: Plugin Installation +slug: discourse-plugin/setup +--- + +Before you can start using the Lexicon Discourse Plugin, there are a few prerequisites and installation steps you need to follow. This documentation will guide you through the process, ensuring a smooth setup of the plugin on your site. + +## Prerequisites + +In order to use this plugin, you must have access to your Discourse server in a way which allows you to modify the server's `app.yml`. If a hosting provider is managing Discourse for you, you will have to contact them to request that they install the plugin on your behalf. + +Specifically, you will need the ability to install plugins, which means directly modifying `/var/discourse/containers/app.yml` to include the [Lexicon Discourse plugin](https://github.com/lexiconhq/discourse-lexicon-plugin.git), and then rebuilding your site. + +## Plugin Installation Steps + +### Access your Server + +Login to your underlying Discourse host server via SSH. + +This is specific to each hosting setup, but typically you will need to use a terminal application such as Terminal on macOS or PuTTY on Windows. + +### Open the Discourse `app.yml` file + +Feel free to use your terminal editor of choice (vim, emacs, nano, etc.). + +:::note +You may need `sudo` access to edit the file, but it depends on how the server was configured. +::: + +```bash +vim /var/discourse/containers/app.yml +``` + +### Get the Plugin’s Git Clone URL + +Discourse plugins are referenced by their reachable Git clone URLs, which typically end with `.git`. + +The Git clone URL for the [Lexicon Discourse plugin](https://github.com/lexiconhq/discourse-lexicon-plugin) can be found below: + +``` +https://github.com/lexiconhq/discourse-lexicon-plugin.git +``` + +Copy it to your clipboard for use in the next step. + +### Add the plugin’s repository URL to your container’s `app.yml` file: + +Add the plugin’s Git clone url to the section below. + +``` +hooks: + after_code: + - exec: + cd: $home/plugins + cmd: + - git clone https://github.com/lexiconhq/discourse-lexicon-plugin.git +``` + +### Rebuild the container, with caution + +:::caution +Please be aware that rebuilding your site will result in your site going offline for a period of time, typically between 5 to 30 minutes. We advise proceeding carefully and taking precautions outlined below. +::: + +#### Precautionary Measures + +1. Before installing the plugin or performing any site rebuild, it is highly recommended to create a backup of your Discourse site. +1. It is advisable to upgrade your Discourse installation and all existing plugins to their latest versions before attempting to install this plugin. +1. Although rare, there may be situations where the site does not come back online after the rebuilding process, and requires further troubleshooting to revive. + - This is always a risk when installing a plugin or performing any task that requires rebuilding the app. + - We recommend that you perform these changes at a time that minimizes the users affected and that you have a well-defined contingency plan in place if something goes wrong. + +#### Run Rebuild Command + +```bash +cd /var/discourse +./launcher rebuild app +``` + +### How to Uninstall the Plugin + +To remove the plugin, simply remove the Git clone URL line from your `app.yml` file and rebuild your site. Please keep in mind that rebuilding your site will will result in your site going offline for a period of time, and poses the same risks that come with rebuilding the app. diff --git a/documentation/versioned_docs/version-2.1.0/discourse-plugin.md b/documentation/versioned_docs/version-2.1.0/discourse-plugin.md new file mode 100644 index 00000000..adf100d2 --- /dev/null +++ b/documentation/versioned_docs/version-2.1.0/discourse-plugin.md @@ -0,0 +1,15 @@ +--- +title: Introduction +slug: discourse-plugin/ +--- + +import useBaseUrl from '@docusaurus/useBaseUrl'; + +--- + +As of Lexicon version 2.0.0, a custom Discourse plugin is available to provide a more seamless mobile integration between Discourse and your Lexicon-powered mobile app. + +The plugin currently offers two key features: + +- **Push notifications**: support for native push notifications on user's mobile devices, according to relevant activity on your Discourse site. Powered by Expo's [push notifications service](https://docs.expo.dev/push-notifications/overview/). +- **Email deep linking**: custom deep links in emails from Discourse, allowing users to seamlessly launch your Lexicon-powered mobile app directly from their mobile email client. diff --git a/documentation/versioned_docs/version-2.1.0/email-deep-linking/intro.md b/documentation/versioned_docs/version-2.1.0/email-deep-linking/intro.md new file mode 100644 index 00000000..ca863368 --- /dev/null +++ b/documentation/versioned_docs/version-2.1.0/email-deep-linking/intro.md @@ -0,0 +1,9 @@ +--- +id: intro +title: Introduction +slug: discourse-plugin/email-deep-linking +--- + +The Lexicon Discourse plugin provides support for integrating Discourse's email notifications with your Lexicon-powered mobile app. Our plugin modifies links in specific Discourse emails so that when a relevant link is tapped and the user has your Lexicon-powered mobile app installed, it will open the app to the relevant topic or post. Otherwise, it will fall back to opening the topic in the device's web browser as it normally would. + +This section of the documentation offers step-by-step instructions to integrate email deep linking into your Discourse site so that your users have a more seamless experience with your Lexicon-powered mobile app. diff --git a/documentation/versioned_docs/version-2.1.0/email-deep-linking/setup/enable-email-deep-linking.md b/documentation/versioned_docs/version-2.1.0/email-deep-linking/setup/enable-email-deep-linking.md new file mode 100644 index 00000000..ec3f749b --- /dev/null +++ b/documentation/versioned_docs/version-2.1.0/email-deep-linking/setup/enable-email-deep-linking.md @@ -0,0 +1,25 @@ +--- +title: Enabling the Lexicon Discourse plugin +--- + +import useBaseUrl from '@docusaurus/useBaseUrl'; + +This guide will walk you through the necessary steps to activate email deep linking on your Discourse site. + +## Steps + +1. Access your Discourse admin dashboard. + +2. Navigate to the `Plugins` section. + + + +3. Locate the `discourse-lexicon-plugin` and click on the `Settings` button. + +4. Fill in the `lexicon app scheme` setting with your app scheme. The app scheme is required to enable email deep linking. + +5. Check the `lexicon email deep linking enabled` box in the Lexicon settings section and save your changes. + + + +Once the email deep linking feature is enabled, you will be able to utilize its functionality in your Discourse instance. diff --git a/documentation/versioned_docs/version-2.1.0/email-deep-linking/setup/verify-email-deep-linking.md b/documentation/versioned_docs/version-2.1.0/email-deep-linking/setup/verify-email-deep-linking.md new file mode 100644 index 00000000..c8c061b8 --- /dev/null +++ b/documentation/versioned_docs/version-2.1.0/email-deep-linking/setup/verify-email-deep-linking.md @@ -0,0 +1,49 @@ +--- +title: Verify Email Deep Linking +slug: verify-email-deep-linking +--- + +import useBaseUrl from '@docusaurus/useBaseUrl'; + +:::note +The steps below assume that **you have already published your Lexicon-powered mobile app** to the App Store and/or Google Play Store **with the correct app scheme**. If you are running the app on your machine locally through Expo, you should not expect the steps to work. +::: + +This guide will provide you with step-by-step instructions to help you validate the functionality of email deep linking within your Lexicon mobile app. + +## Pre-requisites + +:::note +If you have not yet fulfilled all of the pre-requisites below, this test will not work as expected. +::: + +In order to test email deep linking properly: + +1. You **must** have already published your Lexicon-powered mobile app to the App Store and/or Google Play Store. +1. You have already installed and configured the Lexicon Discourse plugin on your Discourse site. +1. You have enabled email deep linking within the Lexicon Discourse plugin settings, and the app scheme matches what you published your app with. +1. You have at least 1 mobile device with your Lexicon-powered mobile app already installed, with the correct app scheme as it was configured in Discourse. +1. You have at least 2 separate Discourse accounts to test with. +1. Ensure your Discourse site allows **mailing list mode**, and that it is turned on for the accounts you are testing with. + - If you do not do this, you will have to wait for Discourse to send its next digest email, which could take a while. + +## Steps + +To test email deep linking within your **published** Lexicon-powered mobile app, follow these steps: + +1. Ensure you have access to at least 2 separate accounts on your Discourse instance. +1. On your mobile device, open your Lexicon-powered mobile app and login using one of your accounts. + - **Note**: ensure that your email client on your mobile device will receive emails for this account. +1. Open your Discourse site in a web browser on your laptop or desktop computer. +1. Login to your **second** Discourse account in your web browser. +1. On your mobile device, using the **first** account, create a new post. +1. Now, on your laptop or desktop computer, using the **second** account, find the post you created on the mobile app and reply to it. +1. Back on your mobile device, you should receive an email notification from Discourse about the reply from the second account. +1. Click on the button that says `Visit Message` or `Visit Topic`. The label depends on what activity generated the email (see screenshot below). +1. The link will first open in your mobile web browser. Provided that the Lexicon-powered mobile app is installed and matches the configured app scheme, it should automatically open your app to the relevant topic or message scene. + +
+ +
+ +And that's it! You have successfully completed the steps to enable and test email deep linking in your app. diff --git a/documentation/versioned_docs/version-2.1.0/env-mobile.md b/documentation/versioned_docs/version-2.1.0/env-mobile.md new file mode 100644 index 00000000..79af9933 --- /dev/null +++ b/documentation/versioned_docs/version-2.1.0/env-mobile.md @@ -0,0 +1,119 @@ +--- +title: Configuration Values +--- + +You can check and set the configuration values in `frontend/Config.ts`. + +The table below describes the configuration values for the Lexicon Mobile App. + +If there is a default value indicated, you do not need to set it. + +| Variable | Required | Notes | Default Value | Example Value(s) | +| -------------------- | -------- | -------------------------------------------------------------------------------------- | ------------- | ------------------------------------------------------------------------------------------------------------------------------------- | +| proseUrl | Yes | The url of the Prose Server (must start with http or https) | - | https://prose.myserver.com https://prose.myserver.com:8080 https://prose.myserver.com/subpath https://prose.myserver.com:8080/subpath | +| inferDevelopmentHost | No | The flag (true / false) to override localhost with the host of the development machine | (empty) | true | + +## The `config` object + +In the `Config.ts` file, you'll find a `config` object that allows you to specify configuration values by scenario. + +The two primary scenarios are: + +- `localDevelopment`: when developing against the app locally. This configuration is also used as a fallback for an unknown build channel. +- `buildChannels`: used to define configuration by build channel when building the app with the EAS CLI. + +Primarily, you'll only be concerned with configuring `proseUrl` for each of these sections. + +## `proseUrl` + +:::caution +`proseUrl` must always be specified, with or without a port number, and must always start with either `http://` or `https://`. +::: + +`proseUrl` is used to specify the URL of the Prose GraphQL API. + +The Prose GraphQL API acts a middleman between the Lexicon Mobile App and your Discourse instance. Without it, the mobile app cannot interact with your Discourse instance. + +### Example + +```ts +const config = { + localDevelopment: { + proseUrl: 'http://localhost:8929', + }, + buildChannels: { + preview: { + proseUrl: 'https://preview.myserver.com', + }, + production: { + proseUrl: 'https://prose.myserver.com', + }, + }, +}; +``` + +With this configuration above, the app will: + +- point at `http://localhost:8929` when you run the app using `npm run start` +- point at `https://preview.myserver.com` when you build the app using `eas build --profile preview` +- point at `https://prose.myserver.com` when you build the app using `eas build` + +`proseUrl` also can include a subpath if desired: + +```ts +const config = { + localDevelopment: { + proseUrl: 'http://localhost:8929', + }, + buildChannels: { + preview: { + proseUrl: 'https://preview.myserver.com:8080/subpath', + }, + production: { + proseUrl: 'https://myserver.com/api/prose', + }, + }, +}; +``` + +**Different Behavior in Development** + +When running the app locally, if `proseUrl` is set to `http://localhost` or `http://127.0.0.1`, it will replace `proseUrl` with the IP address of your development machine. It does this by using Expo's `debuggerHost` constant. + +_Note: this does not apply when building the app._ + +This addresses multiple issues: + +- Accessing `localhost` from within the Android simulator does not map to your development machine +- Accessing `localhost` from a device running Expo Go does not map to your development machine + +Both of these scenarios would otherwise require you to manually identify and specify your development machine's IP address with `proseUrl`. This is bothersome since your machine's IP address can change over time. + +If you are interested in more details about this, the implementation of this behavior is available in `frontend/constants/app.ts`. + +This behavior of automatically overriding those values can be disabled, with `inferDevelopmentHost`, which is covered below. + +## `inferDevelopmentHost` + +:::info +This flag is only valid under `localDevelopment`. It has no effect when used as part of `buildChannels`. +::: + +When in development, by default, the Lexicon Mobile App will check to see if `proseUrl` is set to either `http://localhost` or `http://127.0.0.1`. + +When detected, either of those values will be overwritten with the IP address of your development machine. + +This is a very useful feature that makes on-device testing simply work out of the box without needing to manually specify your IP address (or update it when it changes). + +For scenarios where this behavior is not desirable, `inferDevelopmentHost` can be used as a flag to disable this behavior. It can be disabled by specifying the value as `false`. + +When set to `false`, this behavior of overriding `proseUrl` with the development machine's IP address will no longer occur, and the original value will be passed through as-is. + +```ts +const config = { + localDevelopment: { + proseUrl: 'http://localhost:8929', + inferDevelopmentHost: false, + }, +}; +``` diff --git a/documentation/versioned_docs/version-2.1.0/env-prose.md b/documentation/versioned_docs/version-2.1.0/env-prose.md new file mode 100644 index 00000000..59901fd5 --- /dev/null +++ b/documentation/versioned_docs/version-2.1.0/env-prose.md @@ -0,0 +1,15 @@ +--- +title: Prose Environment Variables +--- + +The table below lays out environment variables for the Prose GraphQL API. + +If there is a default value indicated, you do not need to set it. + +| Environment Variable | Required | Notes | Default Value | Example Value | +| --------------------------- | -------- | ----------------------------------------------------------------------------------- | ---------------------- | ------------------------------------ | +| PROSE_DISCOURSE_HOST | Yes | The specific location of your Discourse instance. | - | https://discourse.example.com | +| PROSE_DISCOURSE_UPLOAD_HOST | No | Instruct Prose to use a different host for file uploads to Discourse. | | https://upload.discourse.example.com | +| PROSE_APP_HOSTNAME | No | The **application-level** hostname that Prose will listen on. | localhost | 0.0.0.0 | +| PROSE_APP_PORT | No | The **application-level** port that Prose will listen on. | 80 | 8080 | +| SKIP_CHECK_DISCOURSE | No | Bypass the startup process of checking the provided Discourse host for reachability | false | true | diff --git a/documentation/versioned_docs/version-2.1.0/intro.md b/documentation/versioned_docs/version-2.1.0/intro.md new file mode 100644 index 00000000..f358899d --- /dev/null +++ b/documentation/versioned_docs/version-2.1.0/intro.md @@ -0,0 +1,142 @@ +--- +id: intro +title: Introduction +slug: / +--- + + + --- iOS Auth + + + + --- iOS Dark Mode + + + + --- iOS Comment + + + + --- iOS Message + + --- Android Auth + + + + --- Android Dark Mode + + + + --- Android Comment + + + + --- Android Message + + + +import useBaseUrl from '@docusaurus/useBaseUrl'; +import Carousel from 'react-bootstrap/Carousel'; + +--- + +Lexicon is a customizable, pre-built mobile app that provides an elegant mobile discussions experience. Built on top of [Discourse](#what-is-discourse). + +## Features + +- Topics, Private Messaging, User Signups, Profile Management, and more +- Rapidly build Android and iPhone apps for your existing Discourse site +- [Push Notifications](./push-notifications/introduction.md) direct to your users' mobile devices +- More seamless native Discourse experience [with Email Deep Linking](./email-deep-linking/intro.md) +- Straightforward process to [**customize**](white-labeling) the app for your brand +- Backed by a [GraphQL](https://graphql.org/) API +- Free and open source! +- [Commercial support](commercial-support) available + +## Benefits + +- Launch a custom mobile discussions app +- Increase engagement with your users by adding a mobile-first Discourse experience—no more [WebViews](https://www.kirupa.com/apps/webview.htm). +- Built with [React Native](https://reactnative.dev/) and [Expo](https://expo.io), delivering a native look-and-feel on both iOS and Android. +- Includes an auto-documented [GraphQL](https://graphql.org/) [interface](concepts#prose-discourse-through-graphql) over the Discourse API, which you can build on top of. + +## Screenshots + +### iOS + + + + IOS Lexicon Login Page + IOS Lexicon Signup Page + IOS Lexicon Home Page + + + IOS Lexicon Dark Mode in Home Page + IOS Lexicon New Post Page + IOS Lexicon Post Detail Page + + + IOS Lexicon Comment Section + IOS Lexicon Profile Page + IOS Lexicon Notification Page + + + IOS Lexicon Message Page + + + +### Android + + + + Android Lexicon Login Page + Android Lexicon Signup Page + Android Lexicon Home Page + + + Android Lexicon Dark Mode in Home Page + Android Lexicon New Post Page + Android Lexicon Post Detail Page + + + Android Lexicon Comment Section + Android Lexicon Profile Page + Android Lexicon Notification Page + + + Android Lexicon Message Page + + + +## How does Lexicon work? + +Lexicon delivers a native mobile Discourse experience with **two key components**: + +- The [**Lexicon Mobile App**](#the-lexicon-mobile-app) - a modern mobile app built with [Expo](https://expo.io) & [React Native](https://reactnative.dev/) +- [**Prose**](#prose-discourse-through-graphql), our GraphQL API on top of the Discourse API + +### The Lexicon Mobile App + +The Lexicon Mobile App is built with [Expo](https://expo.io), which allows us to maintain both the iOS and Android apps with a single codebase. + +For those unfamiliar, Expo provides a superior development and deployment experience on top of [React Native](https://reactnative.dev/). + +### Prose: Discourse through GraphQL + +Prose is Lexicon's [GraphQL](https://graphql.org/) layer built on top of Discourse's API. + +This enables developers to quickly build apps on top of a live Discourse instance while leveraging the [benefits of GraphQL](https://www.apollographql.com/docs/intro/benefits/). + +### What is Discourse? + +Discourse is open-source **discussion software** that is thoughtfully designed, simple to setup, and well-maintained. + +You can learn more about it on the [Discourse website](https://www.discourse.org/). + +### Further Details + +You can learn about the technical details of our approach in [Concepts & Architecture](concepts). + +## License + +MIT. Copyright (c) [Lexicon](https://github.com/lexiconhq) diff --git a/documentation/versioned_docs/version-2.1.0/lexicon-updates.md b/documentation/versioned_docs/version-2.1.0/lexicon-updates.md new file mode 100644 index 00000000..43d95945 --- /dev/null +++ b/documentation/versioned_docs/version-2.1.0/lexicon-updates.md @@ -0,0 +1,17 @@ +--- +title: Receiving Updates from Lexicon +--- + +Due to the nature of this project, the best way to synchronize bugfixes, updates, and other changes to the Lexicon Mobile App is to treat your app like a fork of our repository. + +In the process of customizing the Lexicon Mobile App for your needs, you might make any number of changes to the theme or assets. + +However, the underlying codebase should be—for the most part—untouched. + +When we release a bugfix or new feature on the `master` branch, you'll be able to pull down our changes, resolve any conflicts with your changes, and have an updated version of your app ready to republish. + +It is worth acknowledging that this approach, which effectively uses Git to solve this problem in a fairly simple way, could be improved. + +Provided that there's enough interest, we might later decide to shape Lexicon into more of a standalone SDK package that you can import and receive updates to via npm. + +If you're interested in making that a reality, please reach out to us! diff --git a/documentation/versioned_docs/version-2.1.0/optimal.md b/documentation/versioned_docs/version-2.1.0/optimal.md new file mode 100644 index 00000000..789e1674 --- /dev/null +++ b/documentation/versioned_docs/version-2.1.0/optimal.md @@ -0,0 +1,98 @@ +--- +title: 'Optimal Experience' +--- + +If you're planning to make use of the Lexicon Mobile App, there are a few adjustments you should make to your Discourse instance to provide the best in-app experience to your users. + +## Install the Lexicon Discourse Plugin + +The Lexicon Discourse plugin enhances the native mobile experience for your users in two key ways: + +- Adds support for push notifications +- Adds support for email deep linking. + +You can read more about the plugin and how to set it up [here](./discourse-plugin.md). + +## Enable Topic Excerpts + +We have designed the Mobile App so that users can easily see the first few sentences of a topic as they scroll through the topics list. + +However, by default, Discourse does not return excerpts when listing topics. + +Fortunately, there is a secret setting that enables this. + +It just takes a bit of additional configuration to enable. + +While Discourse does enable opting into this behavior as part of a [Theme Component](https://meta.discourse.org/t/topic-list-excerpts-theme-component/151520), we wanted to guide you through the option of toggling the setting itself. + +Should you prefer to enable it using the above theme component, you're free to do so. + +Enabling this setting involves gaining access to the server and changing a setting. + +### Instructions + +The original instructions can be found [here](https://meta.discourse.org/t/discourse-as-a-simple-personal-blog-engine/138244/4). + +Once you've gained access to your server, enter into the running Discourse app. + +```sh +$ /var/discourse/launcher enter app +``` + +Next, enter the Rails CLI: + +```sh +$ rails c +``` + +Finally, set the setting to true: + +```sh +$ SiteSetting.always_include_topic_excerpts = true +``` + +After that, you can exit, and excerpts should now be displaying in the app. + +## Enable Topic Tagging + +The Lexicon Mobile App was designed with the ability to tag topics in mind. + +This allows users to view and manage tags on topics, which is a popular feature on many Discourse servers. + +Unfortunately, this is not enabled by default. + +### Instructions + +In order to enable it, you can take the following steps: + +- Navigate to the Admin Site Settings page at `/admin/site_settings` +- Use the search bar to search for the setting `tagging enabled` +- Ensure that it is checked +- If you made a change, click the green checkbox button to apply it + +Topics should now be taggable, and viewable in the app. + +## Configure Upload Extensions + +Discourse provides a security feature that allows Discourse admins to specify a whitelist of file extensions that their users can upload. +For example, most admins would choose to restrict uploading of `.exe` files. +In order to be compatible with the settings of your Discourse instance, the Lexicon Mobile App simply requests the list of allowed extensions and uses it to enforce allowed extensions in the app. +Out of the box, most Discourse instances support this default list of extensions: + +- `.jpg` +- `.jpeg` +- `.png` +- `.gif` +- `.heic` +- `.heif` + +If you'd like to adjust the list of extensions in your Discourse instance, you can do so by following the instructions below. + +### Adjusting Allowed Extensions in Discourse + +- Navigate to the Admin Site Settings page at `/admin/site_settings` +- Use the search bar to search for the setting `extensions` +- Find the setting labeled `authorized extensions`. +- Adjust the list as you see fit to include the file extensions you'd like your users to be able to upload. +- When you are done making changes, click the green checkbox to apply them. +- The Lexicon Mobile App will receive the updated list of extensions from your site settings and begin enforcing it for your users. diff --git a/documentation/versioned_docs/version-2.1.0/play-store.md b/documentation/versioned_docs/version-2.1.0/play-store.md new file mode 100644 index 00000000..f329954e --- /dev/null +++ b/documentation/versioned_docs/version-2.1.0/play-store.md @@ -0,0 +1,168 @@ +--- +title: Publishing to the Play Store +--- + +import useBaseUrl from '@docusaurus/useBaseUrl'; + +## Prerequisites + +:::note +If you don't already have a Google Developer account, note that there is a fee to create one. +::: + +- A [Google Developer Account](https://play.google.com/console/signup) to access the [Google Play Console](https://play.google.com/console) +- An Expo account +- EAS CLI 2.6.0 or newer +- The [Lexicon Discourse plugin](./discourse-plugin.md) is already installed on your Discourse instance + +## Google Play Console + +The [Google Play Console](https://play.google.com/console) enables you to setup your app, invite beta testers, and publish your app to the [Google Play Store](https://play.google.com/store). + +Because you're publishing an app that was built using Expo, it is **very important** that you follow [Expo's instructions](https://github.com/expo/fyi/blob/master/first-android-submission.md) for submitting an app to the Google Play store correctly. + +## App Configuration + +After setting up your app in the Google Play Console, there are some other adjustments you'll need to make. + +### Build Config + +Similar to the approach for [Publishing to the App Store](app-store), if you haven’t already, you'll need to set your app name and slug in `frontend/app.json`. The [slug](https://docs.expo.dev/workflow/glossary-of-terms/#slug) is used as part of the URL for your app on Expo's web services, so it is recommended to use kebab-case (e.g., `my-lexicon-app`). + +Replace these placeholders with your desired values: + +:::info +Note below that `scheme` is included. If you want [email deep linking](./email-deep-linking/intro.md) support in your app, **you must specify a scheme**, and then configure the Lexicon Discourse plugin with the same scheme. +::: + +```json +"name": "", +"slug": "", +"scheme": "", +``` + +Then, you need to configure EAS Build by running the following command, or skip to the next [step](play-store#setup-config-values): + +```bash +eas build:configure +``` + +The EAS CLI will prompt you to specify `android.package` and `ios.bundleIdentifier` if those values are not already provided in `app.json`. + +Next, verify that the `package` name and other details specific to your app are included in the `android` section of `app.json`. Note that the `versionCode` will be automatically updated when you build the app with the `production` profile, so you don't need to increment the version manually. + +Also, there's one further detail that you might want to add, depending on your app's permissions. + +In the example below, we're providing our app with the ability to read and write to external storage. + +```json + "android": { + "package": "", + "permissions": [ "READ_EXTERNAL_STORAGE" , "WRITE_EXTERNAL_STORAGE" ] + "versionCode": 1, + }, +``` + +If your app requires further permissions, be sure to specify them as needed in this part of the configuration. + +If you don't quite understand how permissions work yet, it's best to check out the [Expo documentation](https://docs.expo.io/versions/latest/sdk/permissions) on this topic in order to get a full understanding. + +### Setup Config Values + +:::info +When publishing your app, it is necessary to deploy Prose somewhere publicly accessible, perhaps on a cloud hosting provider like AWS or DigitalOcean. If Prose is only running on your local machine, users that download your app won't be able to use it. +Check [the documentation](deployment) to deploy Prose if you haven't already. +::: + +Next, set the **Prose URL** for your builds in `Config.ts`. You can set a different URL for each build channel. + +:::note +In the original release of Lexicon, the **Prose URL** was specified in `frontend/.env`. However, as part of migrating to Expo's EAS feature, we centralized the configuration into `frontend/Config.ts` to save you the trouble of needing to maintain it in more than one place, as suggested in the [Expo documentation](https://docs.expo.dev/build-reference/variables/#can-i-share-environment-variables-defined-in-easjson-with-expo-start-and-eas-update) +::: + +```ts +const config = { + // ... + buildChannels: { + preview: { + proseUrl: 'http://PLACEHOLDER.change.this.to.your.prose.url', + }, + production: { + proseUrl: 'http://PLACEHOLDER.change.this.to.your.prose.url', + }, + }, +}; +``` + +### Add the Play Store Secret File + +For the last step, you'll need to provide a `.json` file containing a private key in order to interact with the Play Store. Follow [this guide](https://github.com/expo/fyi/blob/main/creating-google-service-account.md) to generate one. Then, copy the JSON file to your `lexicon/frontend` directory, and rename the file as `playstore_secret.json`. + +The JSON file looks like this: + +```json +{ + "type": "service_account", + "project_id": "", + "private_key_id": "", + "private_key": "-----BEGIN PRIVATE KEY----------END PRIVATE KEY-----\n", + "client_email": "", + "client_id": "", + "auth_uri": "", + "token_uri": "", + "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", + "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/lexicon%40api.iam.gserviceaccount.com" +} +``` + +## Build your App for Android + +Because we're working with Expo and React Native, this step isn't too different from building your app for iOS. + +From the `frontend/` directory, you can run this command to check the app before publishing: + +```bash +eas build --platform android --profile preview +``` + +Running `eas build` with the `preview` profile will build the app as an APK. This allows you to quickly load it onto your Android device or emulator. After the build is done, navigate to your project in the [Expo web console](https://expo.dev), then click on the **Builds** menu located on the left-hand side of the screen. + +- Click on the project you want to install. + + Builds + +- Download the app by pressing the `Install` button in the `Build Artifact` section. + + Build Artifact + +You can download and launch the app on your real device, or drag the downloaded APK file to your emulator. + +Once you have verified that the app runs as expected, you can proceed to build it for release: + +```bash +eas build --platform android --profile production +``` + +The approach for a production build is similar to the one used for generating a preview build. However, unlike a preview build, you won't be able to launch the production build in Android emulator—it is intended solely for publishing to the Play Store. + +Once this process is completed, you can proceed with submitting it to the Play Store. + +## Publish to the Play Store + +At this point, you can take your app live on the Google Play Store, or you can proceed with internal testing on the Google Play Console. + +To proceed with internal testing, run this command: + +```bash +eas submit --platform android --profile staging +``` + +To release your app publicly, run this command: + +```bash +eas submit --platform android --profile production +``` + +You can read more about build profiles [here](tutorial/publishing). + +At this point, provided that you've completed all the steps, congratulations! Your Lexicon-powered mobile app is now live and ready to be downloaded by your users. diff --git a/documentation/versioned_docs/version-2.1.0/publish-app.md b/documentation/versioned_docs/version-2.1.0/publish-app.md new file mode 100644 index 00000000..267f3e1a --- /dev/null +++ b/documentation/versioned_docs/version-2.1.0/publish-app.md @@ -0,0 +1,11 @@ +--- +title: Publishing your App +--- + +:::danger Progress +This page has not been started yet or needs a lot more work. +::: + +Expo workflow, benefits of, etc. + +Over the air updates? diff --git a/documentation/versioned_docs/version-2.1.0/push-notifications/introduction.md b/documentation/versioned_docs/version-2.1.0/push-notifications/introduction.md new file mode 100644 index 00000000..619429c6 --- /dev/null +++ b/documentation/versioned_docs/version-2.1.0/push-notifications/introduction.md @@ -0,0 +1,8 @@ +--- +title: Introduction +slug: /push-notifications +--- + +The Lexicon Discourse plugin provides support for native push notifications for your Lexicon-powered mobile app. This works for both Android and iOS, and is handled by Expo's [push notifications service](https://docs.expo.dev/push-notifications/overview/). + +This documentation offers step-by-step instructions to seamlessly integrate push notifications into your Discourse site so that your users receive them in your Lexicon-powered mobile app. By following this guide, you will be able to enhance the UX of your users by ensuring they receive timely and engaging notifications about activity on your Discourse site. diff --git a/documentation/versioned_docs/version-2.1.0/push-notifications/plugin-interaction.md b/documentation/versioned_docs/version-2.1.0/push-notifications/plugin-interaction.md new file mode 100644 index 00000000..5a424ac4 --- /dev/null +++ b/documentation/versioned_docs/version-2.1.0/push-notifications/plugin-interaction.md @@ -0,0 +1,31 @@ +--- +title: How Push Notifications work with Lexicon +--- + +import useBaseUrl from '@docusaurus/useBaseUrl'; + +Below, we outline the interaction between the Lexicon mobile app, Prose, and the Discourse Plugin regarding the implementation of push notifications. + +## The Lexicon mobile app + +The Lexicon mobile app plays a crucial role in enabling push notifications for your users. When a user logs into their account using the app, a unique token is generated using the [`expo-notifications`](https://docs.expo.dev/versions/latest/sdk/notifications/) library. This token serves as a unique identifier for the user's device. The app then sends this token to the Prose GraphQL API, which makes a separate request to the Lexicon Discourse plugin. The plugin then inserts a record into your Discourse site's database—ensuring any relevant activity on Discourse triggers a push notification to the user's mobile device. + +## Prose + +As mentioned elsewhere in the documentation, Prose is an intermediary component that facilitates communication between the Lexicon mobile app and your Discourse site. It serves the key role of providing a GraphQL interface over Discourse, which allows the mobile app to communicate with Discourse via GraphQL. + +The latest Prose API exposes a new GraphQL mutation, `pushNotifications`, to receive the unique Expo push token from the mobile app when the user logs in. + +Once Prose receives the token from the app, it forwards the token to the Discourse Plugin running on your site. + +## Discourse Plugin + +The Lexicon Discourse Plugin provides several features. In terms of enabling push notifications, it is responsible for integrating with Expo's [push notifications service](https://docs.expo.dev/push-notifications/overview/). When the Discourse Plugin receives a push token from Prose, it saves the token in your Discourse site's database, associating it with the corresponding user. + +Since the Lexicon Discourse plugin has been configured to respond to events within your Discourse site, it is able to dispatch push notifications based on your users' activity. + +When a relevant event triggers the need for a push notification, such as a new message or reply, the Discourse Plugin retrieves the associated user's token from your Discourse site's database. Using this token, the plugin sends a push notification request to Expo's push notification service, triggering the delivery of the push notification to the user's device. + +## Flowchart + +Build Artifact diff --git a/documentation/versioned_docs/version-2.1.0/push-notifications/setup/enable-push-notifications.md b/documentation/versioned_docs/version-2.1.0/push-notifications/setup/enable-push-notifications.md new file mode 100644 index 00000000..3a21e465 --- /dev/null +++ b/documentation/versioned_docs/version-2.1.0/push-notifications/setup/enable-push-notifications.md @@ -0,0 +1,32 @@ +--- +title: Enable Push Notifications +--- + +import useBaseUrl from '@docusaurus/useBaseUrl'; + + + + + + +Below, we'll walk you through the necessary steps to activate push notifications for your Discourse site. + +## Steps + +1. Ensure the [Lexicon Discourse plugin](../../discourse-plugin-installation.md) is installed and activated. + +1. As an admin user, access your Discourse admin dashboard. + +1. Navigate to the Plugins section. + + + +4. Click on the `Settings` button for the `discourse-lexicon-plugin` entry. + +5. Check the `enable Push Notifications` box in the Lexicon settings section and save your changes. + + + +Once the push notifications setting is enabled, your users will be able to login through the mobile app and start receiving push notifications. + +It is important to remember that push notifications are setup specifically when the user logs in through the mobile app. If users are not receiving push notifications, you should instruct them to log out and log back in before attempting any further troubleshooting. diff --git a/documentation/versioned_docs/version-2.1.0/push-notifications/setup/verify-push-notifications.md b/documentation/versioned_docs/version-2.1.0/push-notifications/setup/verify-push-notifications.md new file mode 100644 index 00000000..7cfe6654 --- /dev/null +++ b/documentation/versioned_docs/version-2.1.0/push-notifications/setup/verify-push-notifications.md @@ -0,0 +1,33 @@ +--- +title: Verify Push Notifications +--- + +import useBaseUrl from '@docusaurus/useBaseUrl'; + + + + + +Below, we'll walk you through how you can validate the functionality of push notifications within your Lexicon-powered mobile app. + +:::info +In order to properly test push notifications, **you will need two separate accounts** on your Discourse site (to generate notifications). + +Additionally, **you will need at least one mobile device** for testing purposes. +::: + +## Step + +To test push notifications within your Lexicon-powered mobile app, follow these steps: + +1. Ensure that you have completed the [Getting Started](../../quick-start) steps for Lexicon. +1. Start the Lexicon Expo app by navigating to `frontend/` and running `yarn start` from your terminal. +1. Using the Expo link or QR Code, launch the app on a real mobile device. +1. Login to the app using one of your accounts. +1. Using that account, create a post within your Discourse site +1. Using a separate account, reply to the post to trigger a notification for the first account. +1. You should receive a push notification on your phone with the reply content from the other account. + + + +And that's it! The Lexicon Discourse plugin is properly sending push notifications through your Discourse site. diff --git a/documentation/versioned_docs/version-2.1.0/quick-start.md b/documentation/versioned_docs/version-2.1.0/quick-start.md new file mode 100644 index 00000000..89f4c8da --- /dev/null +++ b/documentation/versioned_docs/version-2.1.0/quick-start.md @@ -0,0 +1,65 @@ +--- +title: Quick Start +--- + +## Prerequisites + +- Node.js 16.14 or newer +- The latest version of NPM or Yarn, compatible with Node 16.14 or newer +- EAS CLI 3.7.2 or newer to build and publish the app +- An active Discourse site + - If you don’t have one, please follow the instructions in [Development Setup](setup#discourse-host) + +:::note +Follow the instructions in [Setup Guidance](tutorial/setup) to install the prerequisite depedencies, such as NPM and the EAS CLI. +::: + +## Installation + +Clone the repository and navigate into it: + +``` +git clone git@github.com:lexiconhq/lexicon.git +cd lexicon +``` + +Next, install the project's dependencies and generate its GraphQL schema: + +``` +$ npm install && npm run generate +``` + +Note that `npm run generate` involves two steps. + +- First, it will generate a [GraphQL schema](https://nexusjs.org/docs/guides/schema) in the `api` directory. + +- Then, using the generated schema, it will create a new folder called `generated` in the `frontend` directory, containing the resulting query and mutation types. + +- This allows the frontend codebase to stay in sync with, and not duplicate the code for, the types from the `api` directory. + +The code shared from the API is then used by [Apollo](https://github.com/apollographql/apollo-tooling), the GraphQL library we use on the frontend, which enables the Mobile App to query the API correctly. + +## Launch the Mobile App + +You can run the app and test it out by running this command from the project root: + +``` +$ npm run quickstart +``` + +This will simultaneously launch two processes: + +- The Prose GraphQL API Server +- The local Expo dev server, which will enable you to launch the React Native app from your device + +**Please note that this takes some configuration to setup properly**. + +- The `quickstart` command configures the Mobile App and the Prose GraphQL API to point at https://meta.discourse.org, as an example. + +- You'll need to make adjustments to point at a site of your choice. + +- The Lexicon Mobile App (via Expo) must be configured to point at the Prose GraphQL Server + +- The Prose GraphQL Server must be configured to point at an active Discourse instance + +More details are available in the [Development Setup](setup) section diff --git a/documentation/versioned_docs/version-2.1.0/rationale.md b/documentation/versioned_docs/version-2.1.0/rationale.md new file mode 100644 index 00000000..bfb62fa4 --- /dev/null +++ b/documentation/versioned_docs/version-2.1.0/rationale.md @@ -0,0 +1,73 @@ +--- +title: Background & Motivation +--- + +### Discourse's Approach to a Mobile Experience + +Discourse is a phenomenal, battle-tested piece of software that facilitates thoughtful discussions in countless communities around the globe. It's no secret that we are big fans of it. + +The Discourse core team's strategy for mobile devices was to implement their product as a responsive website, and optimize for mobile use cases. This allowed mobile users to simply go to the same Discourse site as they would have on devices with larger screens— enabling them to view and write posts from their mobile devices. + +However, over time, interest in a dedicated Discourse mobile app grew. The core team addressed this need by building a native mobile app. They chose to reuse their existing work by having the app simply wrap a webview containing the mobile site. + +This was a nice improvement, as it allowed the mobile app to integrate with native SDKs and provide some additional features to Discourse mobile users. + +Overall, their approach to solving this problem was both efficient and well-done. + +However, it is still evident to many users that they're interacting with an embedded web browser, and it's clear that it's not a mobile-_first_ experience. + +For many users and site-owners, what the Discourse team has provided is more than enough, and it solves all of their problems. + +In our case, we were looking for a very specific type of experience. + +### Who We Are + +The Lexicon Team is part of [KodeFox](https://www.kodefox.com/), a software studio comprised of passionate software engineers, designers, and product managers who regularly build world-class software for our customers. + +Interested in custom software development with a personal touch? Drop us a line at [hello@kodefox.com](mailto:hello@kodefox.com). + +### Enter Lexicon + +Lexicon was formed out of the desire to further leverage many of the great features that the Discourse team had worked hard to build. + +In our consulting projects, we found that many of our clients were regularly asking for solutions that Discourse already provides out of the box. + +However, our clients wanted a seamless, native mobile experience, tailored to the brand that their users were already familiar with. + +After digging into the Discourse API documentation, we felt that it was worthy investment to build a mobile-first Discourse experience which also faciliated customizability. + +We were already fluent with the elegant development process provided by React Native and Expo, so it was a natural fit for us to build the mobile app with these technologies. + +This allowed us to achieve a high ratio of code reuse across iOS and Android, making feature implementations and bug fixes a much simpler process in most cases. + +In integrating with Discourse's API, we also noticed that the API documentation contains a disclaimer which encourages reverse-engineering to understand it. + +While we can appreciate the sentiment of figuring things out yourself, we wanted to provide an API experience that makes it easy for developers to dig into interactive documentation and quickly grasp the concepts. + +For this reason, we also chose to build Prose, our GraphQL API layer on top of the Discourse RESTful API. Another motivating factor was our existing fluency with GraphQL. + +This allowed us to quickly implement the mobile app with an intuitive API paradigm that we were already very familiar with. + +#### How Lexicon can help you + +If you already run an existing Discourse site and want a native mobile experience for your users, you can very quickly point Lexicon at your site and browse it in real-time from your device. + +Check out the [Quick Start](quick-start) page to see a rapid example of spinning up a mobile app for Discourse's own [Meta site](https://meta.discourse.org). + +But beyond that, Lexicon is an open source pre-built mobile app. This means that you can customize it to fit your brand. + +You can think of it like a template that you can use to build your own mobile app for your community. + +If you're interested in customizing the Lexicon Mobile app, you can learn more about that in the [White Labeling](white-labeling) section. + +And when you're finished, you can publish it to the Apple App Store or Google Play Store, which we cover in [Publishing your App](app-store). + +### FOSS Mindset + +Finally, while this project will benefit us and our clients in the future, we also wanted it to be a gift to the community. + +We recognize and support the culture of free and open-source software. That's why we're delighted to give back to the community in this way, just as the Discourse team originally did when they chose to open-source their hard work. + +So please engage with us on Github, and don't be shy about opening a new issue or even a PR. + +We look forward to working with you! diff --git a/documentation/versioned_docs/version-2.1.0/setup.md b/documentation/versioned_docs/version-2.1.0/setup.md new file mode 100644 index 00000000..99305a33 --- /dev/null +++ b/documentation/versioned_docs/version-2.1.0/setup.md @@ -0,0 +1,376 @@ +--- +title: Development Setup +--- + +### Clone the Lexicon Repository + +If you haven't already, make sure you [clone the Lexicon repository](quick-start#installation) from Github. + +### Setup a Discourse Instance, if necessary + +In order to get started developing against the Lexicon Stack, you'll need a running Discourse instance. + +To recap, the Lexicon Stack consists of: + +- The Lexicon Mobile App +- The Lexicon Prose GraphQL API +- A running Discourse instance + +Without a Discourse instance, the Prose GraphQL API has nowhere to retrieve data from. And when the Prose GraphQL API can't retrieve any data, the Lexicon Mobile App won't be able to receive anything either. + +For detailed instructions on setting up a local development instance of Discourse, head over to the [tutorial](./tutorial/setup-discourse), which will walk you through the process. + +However, if you already have a deployed instance of Discourse, we'd recommend using that instead. + +### Install the Lexicon Discourse Plugin + +The Lexicon Discourse Plugin is a Discourse plugin that adds support for [push notifications](./push-notifications/introduction.md) and [email deep linking](./email-deep-linking/intro.md). + +You can install the plugin in your Discourse instance by following the instructions in the [Discourse plugin documentation](./discourse-plugin.md). + +For local development, you're only able to test out push notifications, as email deep linking requires a published app with a [valid app scheme](https://docs.expo.dev/versions/latest/config/app/#scheme). + +If you wish to develop against the plugin itself, you can clone the codebase [here](https://github.com/lexiconhq/discourse-lexicon-plugin.git). + +### Configuration + +The [Lexicon Stack](concepts#architecture-of-the-lexicon-stack) requires some configuration in order to properly interact with your Discourse server. + +This involves configuring both the backend GraphQL API, which interacts with your Discourse instance; as well as the frontend Mobile App, which interacts with the GraphQL API. + +The architecture of this setup is depicted in [Architecture of the Lexicon Stack](concepts#architecture-of-the-lexicon-stack). + +#### Backend GraphQL API Configuration + +The [Prose GraphQL API](concepts#prose-discourse-through-graphql) is fairly simple in terms of configuration. In the simplest case, it only needs to know where your Discourse instance is accessible at. + +It receives its configuration via a [`.env` file](https://www.codementor.io/@parthibakumarmurugesan/what-is-env-how-to-set-up-and-run-a-env-file-in-node-1pnyxw9yxj) in the root of the `api/` directory. + +Here is the simplest configuration of the `api/.env` file: + +``` +PROSE_DISCOURSE_HOST=https://meta.discourse.org +``` + +It is also worth noting that you can optionally configure the **Hostname** and **Port Number** that the Prose API server listens on, both of which default to **localhost** and **port 80**, respectively. + +``` +PROSE_DISCOURSE_HOST=https://meta.discourse.org + +# Instruct Prose to broadcast publicly instead of on localhost +PROSE_APP_HOSTNAME=0.0.0.0 + +# Instruct Prose to listen on port 8929 instead of the default port 80 +PROSE_APP_PORT=8929 +``` + +For a comprehensive list of all environment variables that can be used to configure Prose, check out [Prose Environment Variables](env-prose). + +#### Frontend Mobile App Configuration + +:::note +In the original release of Lexicon, the **Prose URL** was specified in `frontend/.env`. However, as part of migrating to Expo's EAS feature, we centralized the configuration into `frontend/Config.ts` to save you the trouble of needing to maintain it in more than one place, as suggested in the [Expo documentation](https://docs.expo.dev/build-reference/variables/#can-i-share-environment-variables-defined-in-easjson-with-expo-start-and-eas-update) +::: + +To configure the frontend mobile app, you'll first need to set your app name and slug in `frontend/app.json`. The [slug](https://docs.expo.dev/workflow/glossary-of-terms/#slug) is used as part of the URL for your app on Expo's web services, so it is recommended to use kebab-case (e.g., `my-lexicon-app`). + +Replace these placeholders with your desired values: + +```json + "name": "", + "slug": "", +``` + +Next, change the value of `proseUrl` in `frontend/Config.ts` to the URL of your Prose GraphQL API—whether local or already deployed somewhere. + +```ts +const config = { + localDevelopment: { + proseUrl: 'http://localhost:8929', + }, + buildChannels: { + preview: { + proseUrl: 'https://preview.myserver.com:8080/subpath', + }, + production: { + proseUrl: 'https://myserver.com/api/prose', + }, + }, +}; +``` + +`localDevelopment.proseUrl` will be used during development when you run the app using `npm run start` or `expo start`, whereas the specific value within `buildChannels` (e.g., `production.proseUrl`) will be used when actually building the app. + +#### Development Scenarios + +When developing locally, there are at least three scenarios that you may find yourself in. + +Depending on which one applies to you, the config values across `frontend/Config.ts` and `api/.env` may need to be set differently. + +##### Scenario 1: Existing Prose Deployment + +If you've already deployed the Prose GraphQL API to a host that is publicly reachable, you will have already setup `api/.env` with the proper values. + +In that case, `frontend/Config.ts` only needs updated to point at the deployed GraphQL API. + +For example: + +```ts +const config = { + localDevelopment: { + proseUrl: 'https://my-deployed-graphql.api', + }, + buildChannels: { + preview: { + proseUrl: 'https://my-deployed-graphql.api', + }, + production: { + proseUrl: 'https://my-deployed-graphql.api', + }, + }, +}; +``` + +In the example above, we have configured the app to point at `https://my-deployed-graphql.api` in all scenarios, including during development when running with `npm run start`. + +##### Scenario 2: Run Prose Locally & Access from a Simulator + +:::info +If you are running the Prose server locally, you should not expect that the mobile app will continue to function if you turn off your development machine. You must **deploy** the server before attempting to use the mobile app without depending on your development machine. +::: + +This approach involves running both the Lexicon Mobile App and the Prose GraphQL API on your development machine. It is accomplished by instructing Expo to launch the Mobile App in the Android or iOS simulator. + +When developing this way, you can simply set `localDevelopment.proseUrl` to `http://localhost` in `frontend/Config.ts`. And then in `api/.env`, you can set `PROSE_APP_HOSTNAME` to `0.0.0.0`. + +Note that if you want to run Prose locally on a specific port, you would need to make sure that the configuration in both `api/.env` and `frontend/Config.ts` reflect that correctly. + +:::caution +If you configure `PROSE_APP_HOSTNAME` in `api/.env` to only listen on `localhost` or `127.0.0.1` (rather than `0.0.0.0`), it prevents others on the same network as your development machine from accessing it. This includes both your mobile device and the Android simulator, which can lead to connectivity issues when developing locally. +::: + +##### Scenario 3: Run Prose Locally & Access from your Mobile Device + +It can be very useful to develop and debug against the app using your actual mobile device with the [Expo Go app](https://expo.dev/client). + +In order to do this, you'll need to have your development machine reachable from your mobile device. + +A simple way to make it reachable is to ensure that your mobile device and development machine are on the same network, and then, in `api/.env`, set `PROSE_APP_HOSTNAME` to `0.0.0.0`. + +In a regular Expo project, you would be required to update the `localDevelopment.proseUrl` value in `frontend/Config.ts` to contain the hardcoded IP address of your development machine on your network. + +However, by setting the value to `http://localhost`, we handle this **automatically** by default, so you don't have to worry about it. Read more about it [here](env-mobile#infer_development_host). + +###### Hardcoding your local IP Address + +:::info +This approach is not ideal. If your local IP address ever changes, you'll need to locate it again, and update `Config.ts` to reflect that. For this reason, it's preferable to just use `http://localhost`. +::: + +To manually instruct the Mobile App how to locate your development machine, you'll need to find out what the **local IP address** of your development machine is on your current network. + +Note that your local IP address is different from your public IP Address. + +If you are not sure how to get your local IP address, you can go to [What Is My Browser: Detect Local IP Address](https://www.whatismybrowser.com/detect/what-is-my-local-ip-address) and follow the instructions. + +The website itself may not be able to automatically detect your local IP address, but it will give you instructions on how to locate it within your specific operating system. + +You will be given an IP address like `10.0.12.121` or `192.168.17.69`. + +You can then update the value in `frontend/Config.ts` to your local IP address. + +This will allow the app running on your mobile device to properly locate the GraphQL API running on your development machine. + +## Configure your Discourse Host + +As mentioned above, you'll need to have setup a Discourse host for the GraphQL API to interact with. + +We'd like to briefly cover the different approaches to setting up a Discourse Host for development before continuing. + +**1. Run a Discourse Instance Locally** + +:::note +Ensure that you are managing all of your ports correctly. + +The development setup of Discourse with Docker makes use of multiple ports, one of which being **port 3000** by default. You'll want to double-check that none of the environment variables are pointing at the ports Discourse is using. +::: + +If you'd like to run a Discourse site for development locally, the recommended way to do this to use **[Docker](https://www.docker.com/)**, so make sure you have it installed. + +Then, as we mentioned above, you can follow [these steps in the tutorial](tutorial/setup-discourse) to install and run a development instance of Discourse in Docker. + +**2. Use try.discourse.org or another popular Discourse site** +:::info +Feel free to use existing public Discourse sites—such as the [Docker Community Forum](https://forums.docker.com/) or the [Rust Programming Language Forum](https://users.rust-lang.org/)—in order to test out the Lexicon Mobile App. + +Just be mindful of how you're contributing to those sites if you do. +::: + +[Try Discourse](https://try.discourse.org/) is a publicly accessible Discourse instance which is intended for testing. As such, it resets every day. + +The only drawback of this approach is that you can only register as a normal user, and therefore cannot modify the site's admin settings. + +With this approach, you'd simply configure Prose in `api/.env` to point `PROSE_DISCOURSE_HOST` at one of these instances. + +```bash +PROSE_DISCOURSE_HOST=https://try.discourse.org +``` + +## Working with the Codebase + +Now that you've prepared everything for development, you can start digging in on the Lexicon codebase. + +### Run the Lexicon Mobile App & Prose GraphQL Server + +You can run the Mobile App and test it out with a local Prose server by running this command **from the project root**: + +``` +$ npm run dev +``` + +This will simultaneously launch two processes: + +- The GraphQL API Server +- The local Expo dev server, which will enable you to launch the React Native app from your device + +However, if you wish to run the frontend and backend seperately, execute the following command in a terminal to run the frontend + +``` +$ npm run --prefix frontend start +``` + +Then execute the following line in another terminal to run the backend + +``` +$ npm run --prefix api dev +``` + +### Debugging + +- Use [Expo Developer Menu](https://docs.expo.io/workflow/debugging/#developer-menu) to make the debugging process easier. + +Opening the Expo Developer Menu depends on your device: + +- On an iOS Device: Shake the device, or touch 3 fingers to the screen. +- On the iOS Simulator: Hit `⌘ + ctrl + Z` on a Mac in the emulator. +- On an Android Device: Shake the device vertically, or run `adb shell input keyevent 82` in the terminal window if the device is connected via USB. +- On the Android Emulator: Hit `⌘ + M`, or run `adb shell input keyevent 82` in your terminal window. + +- If your changes don't show up, it could involve a cache issue. In this case, you should try restarting Expo. + - To do so, quit the process by hitting `Ctrl + C` in the Terminal where it is running. + - Then run `npm run start` again. + - If the issue persists, you should look for the latest guidance from Expo on how to clear the cache, as it has been known to change. + +### Running the Test Suites + +Before running tests, double-check that your changes don't contain any errors. + +You can run tests across both the frontend and backend codebases sequentially by running the following command from the project root: + +``` +$ npm run test +``` + +On top of ensuring that all tests have passed, the command will also notify you if there are any Typescript errors or issues from Prettier or ESLint. + +Also note that the process of running `npm run test` triggers an additional action in the frontend to take place before running the tests. + +A new folder, `frontend/generated`, is created and populated with all the GraphQL Query and Mutation types for use in the codebase. + +If we did not run this before the tests, they would fail due to type errors. + +### Build & Publish the Lexicon Mobile App + +:::note +An Expo account is required in order to use Expo's services. You can create one here: https://expo.io/signup. +Once you have created your Expo account, please ensure that you are signed in with your current shell session, via `expo login` or `eas login`. +::: + +You are required to configure EAS build first by running: + +```bash +eas build:configure +``` + +You will then get a prompt from the EAS CLI related to the EAS project IDs: `android.package` and `ios.bundleIdentifier`. EAS will provide you with an existing project ID if you have one or ask you to create a new one. As for `android.package` and `ios.bundleIdentifier`, you can specify those values with `com.companyname.appname`, or any other patterns you might prefer. + +Once you're done, verify the `proseUrl` value you will use for the actual build of the app in `Config.ts`. + +:::info +When publishing your app, it is necessary to deploy Prose somewhere publicly accessible, perhaps on a cloud hosting provider like AWS or DigitalOcean. If Prose is only running on your local machine, users that download your app won't be able to use it. +Check [the documentation](deployment) to deploy Prose if you haven't already. +::: + +Now you can build the Mobile App via Expo (EAS) with the preview build profile by running command below: + +```bash +eas build –platform all –profile preview +``` + +When you do this, the packager will minify all your code and generate two versions of your code—one for iOS, and one for Android—and then upload them both to the Expo CDN. + +Additionally, if you haven't yet optimized the app's assets, Expo will ask you if you'd like to do so. + +This has the same effect as manually running `npx expo-optimize` beforehand. It simply compresses all of the image assets in your project to reduce the size of your build. + +When the process is complete, you'll be presented with a shareable QR Code and a URL resembling https://exp.host/@ccheever/an-example, which directs you to the build details in Expo's web console. + +At this point, anyone can then use that link to load your project. + +For Android, you can install the app on an emulator or on your physical device. However, for iOS, you can only install it on the iOS simulator. To run the app on a real iOS device, follow the steps in [this part](tutorial/building#1-preview) of the tutorial. + +When building your app, it is recommended to build it as a preview build first, and make sure everything runs well before building it for release with the production profile. + +To build the app with the production build profile, run this command: + +```bash +eas build –platform all –profile production +``` + +You will also be presented with links directing you to the build details in Expo. + +However, unlike the preview build, the release build cannot be installed directly on your physical device or in an emulator / simulator. You'll need to publish the app and then install it from either the Play Store or App Store. + +You can read a more detailed explanation of this process in [this section](tutorial/building) of the tutorial. + +#### Updates + +If you later want to deploy an update to your version of the Lexicon Mobile App, you can use the EAS update command. + +First, make sure to configure EAS update by running the following command: + +```bash +eas update:configure +``` + +This command will automatically add the `expo.runtimeVersion` field to your `app.json` file. +You'll see a warning in your terminal telling you to add `expo.updates.url` to `app.json`. + +Then run this command to update your project: + +```bash +eas update -–branch +``` + +:::note +The channel name is the same as the build profile, so for the preview builds, you can run: + +```bash +eas update -–branch preview +``` + +::: + +Read more about updating your app [here](tutorial/updating). + +Once published, the new version will be available to your users the next time they open it. + +For more details on this process—including publishing to the App Store and Google Play Store—follow the instructions in [Publishing your App](tutorial/publishing). + +#### Configure the GraphQL API with your Discourse Server + +In order for a published version of the app to be able to contact your Discourse server, you'll need to ensure that: + +- The GraphQL API is deployed and running properly on a host that is reachable from the app itself. +- The GraphQL API is configured to point at the correct host and port of your Discourse server +- Your Discourse server is reachable by the GraphQL API diff --git a/documentation/versioned_docs/version-2.1.0/supported-devices.md b/documentation/versioned_docs/version-2.1.0/supported-devices.md new file mode 100644 index 00000000..84e6b1bb --- /dev/null +++ b/documentation/versioned_docs/version-2.1.0/supported-devices.md @@ -0,0 +1,33 @@ +--- +title: Supported Devices +--- + +import useBaseUrl from '@docusaurus/useBaseUrl'; + +## iPhone and Android Phones + +:::info +Older versions of iOS and Android may work, but are not officially supported. +::: + +Once you've published to the App Store and Google Play Store, your published app will work out of the box for your users on both iPhone and Android devices with the following specifications: + +| Device | Minimum OS | +| --------------- | -------------------- | +| iPhone | iOS 16 and above | +| Android Devices | Android 13 and above | + +| Android | iOS | +| -------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | +| | | +| | | +| | | +| | | + +## Support for Other Devices + +At this time, **tablets - including iPads** - and other mobile devices are **not supported**. + +We may consider developing support for this in the future. + +If this is critical for you, please drop us a line at support@kodefox.io and let us know. diff --git a/documentation/versioned_docs/version-2.1.0/technologies.md b/documentation/versioned_docs/version-2.1.0/technologies.md new file mode 100644 index 00000000..8657288a --- /dev/null +++ b/documentation/versioned_docs/version-2.1.0/technologies.md @@ -0,0 +1,21 @@ +--- +title: Technologies +--- + +### 100% React Native and TypeScript built on Expo + +Lexicon was built, and is maintained, with a single code base—meaning that bug fixes, improvements, and new features will (in most cases) automatically apply to both iOS and Android. + +### GraphQL-based API + +Developers who wish to contribute to (or fork) Lexicon can do so with all the benefits of GraphQL. For more information, check out [Concepts and Architecture](concepts#prose-discourse-through-graphql). + +### White Labeling Support + +White Label the Lexicon Mobile App to give your users the familiar look and feel of your brand. Learn more in [White Labeling](white-labeling). + +### Painless integration with existing Discourse instances + +Getting started is as easy as spinning up a new server for the Prose GraphQL API, and pointing it at your Discourse instance. No changes are required on your Discourse instance itself. + +Note: to enable features like [Push Notifications](./push-notifications) and [Email Deep Linking](./email-deep-linking/intro.md), you can install our [Discourse Plugin](./discourse-plugin.md). diff --git a/documentation/versioned_docs/version-2.1.0/theming.md b/documentation/versioned_docs/version-2.1.0/theming.md new file mode 100644 index 00000000..04dbc172 --- /dev/null +++ b/documentation/versioned_docs/version-2.1.0/theming.md @@ -0,0 +1,247 @@ +--- +title: Theming +--- + +:::note +This section will involve reading and modifying Typescript. If you get stuck, reach out to us. +::: + +Lexicon allows you to customize the default theme that the Mobile App provides. + +You can accomplish this by modifying the values in `frontend/src/constants/theme`, or in `frontend/src/theme`. + +There is a difference between the two, and they work in conjunction with one another. + +`frontend/src/constants/theme` defines the underlying base values of the theme. + +`frontend/src/theme` then imports those values, and uses them to compose the actual theme object used throughout the rest of the Mobile App. + +## Colors + +### Adjusting Base & Functional Colors + +There are 2 types of colors in the Mobile App: base colors and functional colors. + +Base colors are the underlying palette of the theme, whereas functional colors define specific use-cases of the base colors. + +For example, you might have noticed that the Mobile App features a nice, eye-catching Royal Blue color as its primary color. + +This is defined in the base colors as: + +```ts +// ... +royalBlue: '#2B6AFF', +// ... +``` + +Then, the functional colors make use of this for particular components in the app. + +To continue with the example, the `royalBlue` base color is referenced in the functional colors as: + +```ts +// ... +activeTab: BASE_COLORS.royalBlue, +// ... +primary: BASE_COLORS.royalBlue, +// ... +``` + +Now, any component can reference the functional colors' `primary` value, and it will be `royalBlue`. + +However, if you wanted a different theme with a new color, such as, `BASE_COLORS.lightningYellow`, then you could adjust it to: + +```ts +// ... +activeTab: BASE_COLORS.lightningYellow, +// ... +primary: BASE_COLORS.lightningYellow, +// ... +``` + +And the Mobile App would replace the Royal Blue with the value you've defined for Lightning Yellow. + +For this reason, if you want to add more colors, you'll need to add base color values first, and then access them within the functional colors. + +This approach keeps a clean separation of concerns, which allows theme changes to seamlessly propagate throughout the Mobile App. + +### Color Scheme (Dark Mode and Light Mode) + +The theme allows you to control how the user can adjust the app's color scheme, if at all. + +There are three choices for this: `dark`, `light`, `no-preference`. + +- Dark: force the color scheme to remain dark +- Light: force the color scheme to remain light +- No Preference (default): allow your users to specify a preference for color scheme + +Note that if you specify `dark` or `light`, your users **will not** have the option of selecting a preference for color scheme. + +This manifests in the Mobile App by hiding the Dark Mode button which normally appears in the Preferences Scene. + +## Fonts + +The theme's fonts are declared in `frontend/src/constants/theme/fonts`. + +Inside of that file, you'll find multiple aspects of the fonts that can be adjusted: + +- Font Variants +- Font Sizes +- Heading Font Sizes + +### Font Variants + +Used to classify multiple font weights into named variants. It supports the following values: + +| Variants | Default font weight | +| -------- | ------------------- | +| bold | 700 | +| semiBold | 600 | +| normal | 400 | + +### Font Sizes + +Used to set a font size scale that is consistent throughout the app. It supports the following values: + +| Variants | Default size | +| ---------------- | ------------ | +| xl (extra large) | 24 | +| l (large) | 18 | +| m (medium) | 16 | +| s (small) | 14 | +| xs (extra small) | 12 | + +### Heading Font Sizes + +Used to classify multiple font sizes for heading elements, such as `h1`, `h2`, etc. + +These values are primarily used for rendering the content of posts and messages from Discourse. + +This is because Discourse posts are written in Markdown, and users will often leverage heading elements to format their posts. + +| Variants | Default size | +| -------------- | ------------ | +| h1 (Heading 1) | 32 | +| h2 (Heading 2) | 24 | +| h3 (Heading 3) | 22 | +| h4 (Heading 4) | 20 | +| h5 (Heading 5) | 18 | +| h6 (Heading 6) | 17 | + +## Icons + +The `icons` theme file is used to store icon-related constants. + +Currently, the ‘icons’ file only contains a constant which declares the icon sizes scale. + +| Variants | Default size | +| ---------------- | ------------ | +| xl (extra large) | 28 | +| l (large) | 24 | +| m (medium) | 20 | +| s (small) | 18 | +| xs (extra small) | 16 | + +## Images + +The `images` theme file is used to store theme constants used in rendering images. + +Currently, this file declares the following theme values: + +- Avatar Icon Size +- Avatar Letter Size +- Avatar Image Size + +Avatars are used throughout the app to display relevant info about a post or message. + +As such, it is typically the user's photo. + +However, when a photo is not provided, we also compose a letter-based avatar based on the user's initials. + +### Avatar Icon Size + +| Variants | Default size | +| ---------------- | ------------ | +| l (large) | 96 | +| m (medium) | 52 | +| s (small) | 40 | +| xs (extra small) | 28 | + +### Avatar Letter Size + +| Variants | Default size | +| ---------------- | ------------ | +| l (large) | 72 | +| m (medium) | 36 | +| s (small) | 28 | +| xs (extra small) | 16 | + +### Avatar Image Size + +This defines the quality of the image used for avatars. + +| Variants | Default size | +| ---------------- | ------------ | +| xl (extra large) | 450 | +| l (large) | 150 | +| m (medium) | 100 | +| s (small) | 50 | + +## Spacing + +The `spacing` theme file defines spacing constants used throughout the Mobile App for padding and margins. + +| Variants | Default size | +| ------------------------- | ------------ | +| xxxl (triple extra large) | 36 | +| xxl (double extra large) | 24 | +| xl (extra large) | 16 | +| l (large) | 12 | +| m (medium) | 8 | +| s (small) | 4 | +| xs (extra small) | 2 | + +## Advanced Customization + +While the above adjustments are generally fairly simple, you can really customize the Mobile App to your heart's content (based on your skill level). + +Here are some additional aexamples. + +### Custom Fonts + +#### Create a folder for the Custom Fonts + +To keep the codebase organized, create a folder named `fonts` inside of `frontend/assets`. + +You can then move your custom font assets into this folder. + +#### Install & Use the `expo-font` Package + +This package eases the process of adding custom fonts into an Expo-based app. + +In particular, you'll want to use the `loadAsync` function from it, which will map your font assets to their variant names throughout the Mobile App. + +While we won't get into too much technical detail here, their [documentation](https://docs.expo.dev/versions/latest/sdk/font/) can guide you through the process. + +### Error Messages + +It is possible to customize both the error messages and the means through which they are presented to the user. + +In order to do this, you should first be aware of two files. + +#### `frontend/src/helpers/errorMessage.ts` + +The Prose GraphQL API forwards on error messages from Discourse. + +This file declares the specific text of those messages as constants so that they can easily be compared in `errorHandler.ts`. + +If you observe any additional error messages that are not being caught, you'll want to add them to this file, and then adjust `errorHandler.ts` below accordingly. + +#### `frontend/src/helpers/errorHandler.ts` + +This file imports from the above `errorMessage.ts`. + +It then defines exactly how errors should be handled, including the above messages, when they are encountered. + +Currently, the default approach is to display the errors using an Alert to the user. + +However, if you wanted to integrate snackbars, you would adjust the code in `errorHandler.ts` to replace the invocations of `Alert.alert`. diff --git a/documentation/versioned_docs/version-2.1.0/troubleshooting-build.md b/documentation/versioned_docs/version-2.1.0/troubleshooting-build.md new file mode 100644 index 00000000..68d75dad --- /dev/null +++ b/documentation/versioned_docs/version-2.1.0/troubleshooting-build.md @@ -0,0 +1,128 @@ +--- +title: Troubleshooting when trying out the app +--- + + + + + +import useBaseUrl from '@docusaurus/useBaseUrl'; + +## Troubleshooting Connection and Configuration Issues with URL + +
+ please connect to network error +
+ +If you are encountering issues related to the URL, resulting in an error message saying "please connect to network" as shown in the screenshot, it is likely due to incorrect settings. Specifically, if you are attempting to test builds locally on your mobile device and the channel field is not properly configured, the app may continuously fallback to the localDevelopment channel, even if you have set it to something else like "preview." + +Here some steps and notes to help resolve this: + +- Open the `frontend/Config.ts` file in your project. +- Locate the `config` object within the file. +- In the `localDevelopment` section of the `config` object, you can add the Prose URL specific to the channel you are trying to test. This section is used for local development and as a fallback configuration for unknown build channels in EAS Build. Here's an example: + + ```ts + const config: Config = { + localDevelopment: { + proseUrl: 'http://localhost:8929', + }, + buildChannels: { + preview: { + proseUrl: 'http://PLACEHOLDER.change.this.to.your.prose.url', + }, + production: { + proseUrl: 'http://PLACEHOLDER.change.this.to.your.prose.url', + }, + }, + }; + ``` + +- The example above shows that the config consists of two main sections: localDevelopment, which specifies the URL during localDevelopment, and buildChannels, which includes configurations for different channels such as preview and production. For local development, it will hit the Prose API with the URL `http://localhost:8929`. If the buildChannel is unknown or not found, it will always default to localDevelopment. +- Update the `proseUrl` value within the desired build channel, such as `preview` or `production`, with the valid and reachable URL of your Prose server. +- Once you have made the necessary changes, save the `frontend/Config.ts` file. + +Now, when you run eas build for a specific build channel, such as `eas build --profile=production`, it will utilize the Prose URL specified in the production configuration. + +:::note +It is important to include the URL in the `frontend/app.json` file, which expo-updates will use to fetch update manifests. Failing to set the URL in the `frontend/app.json` file will result in the expo-update constant always returning undefined for the channel, causing the app to consistently utilize the localDevelopment URL after building. You can specify this URL in the expo and updates sections of the app.json file. For more detailed information on how to configure this, please refer to the [expo documentation](https://docs.expo.dev/versions/latest/config/app/#url) for more detail on this. + +```json +"expo": { + "updates": { + ..., + "url": "https://u.expo.dev/" + } +} +``` + +This configuration is essential for seamless integration with Config.ts in your project. +::: + +In certain cases, you may encounter an issue related to Prose API URLs when the channel name specified in the `frontend/eas.json` file does not match the corresponding key name defined in the `config` variable in `frontend/Config.ts`. This discrepancy can lead to problems because the channel name from `eas.json` is used to determine the URL that will be utilized. If the names do not match, the default `localDevelopment` URL will be used instead. + +To ensure smooth functioning, it is important to use the same channel name in both the `frontend/eas.json` file and the `frontend/Config.ts` file. This will ensure proper mapping of the channel name to the corresponding URL. + +Here is an example to illustrate this: + +```json +// frontend/eas.json + +"build": { + "staging": { + "android": { + "buildType": "apk" + }, + "channel": "staging" + } +} +``` + +```ts +// frontend/Config.ts; + +const config: Config = { + localDevelopment: { + proseUrl: 'http://localhost:8929', + inferDevelopmentHost: true, + }, + + buildChannels: { + preview: { + proseUrl: '', + }, + production: { + proseUrl: '', + }, + staging: { + proseUrl: '', + }, + }, +}; +``` + +## The app closes abruptly after the splash screen + +If you encounter a situation where your app closes abruptly after the splash screen, it is likely that there are missing configurations in your `app.json` file. One common cause is the absence of a scheme definition in `app.json`, which is essential during the app build process. + +To resolve this issue, follow these steps: + +1. Open your project's `frontend/app.json` file. +2. Look for the `"expo"` section. +3. If a scheme is not present add this part in `"expo"` section + +```json +"expo":{ + "name": "", + "slug": "", + "scheme": "", + "version": "1.0.0" +} +``` + +Replace `""` with the desired scheme name for your app. + +4. Save the changes to the `app.json` file. +5. Rebuild your app and test it again. + +By ensuring that the scheme is correctly defined in `app.json`, you should be able to resolve the issue of the app closing after the splash screen. diff --git a/documentation/versioned_docs/version-2.1.0/tutorial/building.md b/documentation/versioned_docs/version-2.1.0/tutorial/building.md new file mode 100644 index 00000000..3e261343 --- /dev/null +++ b/documentation/versioned_docs/version-2.1.0/tutorial/building.md @@ -0,0 +1,152 @@ +--- +title: Build your App +--- + +## EAS Build + +EAS Build is the upgraded version of `expo build`. This service helps to build app binaries for your Expo and React Native projects. Read more about it in the Expo documentation [here](https://docs.expo.dev/build/introduction/). + +### Configuration + +Let's get started by configuring EAS build. Check [here](https://docs.expo.dev/build-reference/build-configuration/) to see the complete guide from Expo. + +#### Build Setup + +Run this command in `/frontend` directory: + +```bash +eas build:configure +``` + +When running that command, the EAS CLI will typically do the following: + +1. It will prompt you for the EAS project ID, either to use an existing ID if you have one, or create a new one. Then it will automatically add the `expo.extra.eas.projectId` field in `app.json`. +2. It will create a new `eas.json` file if one doesn’t already exist. However, we have that set up for you, so you don't need to worry about creating one. 🎉 +3. It will prompt you to specify `android.package` and `ios.bundleIdentifier` if those values are not already provided in `app.json`. Note that those two values don't have to be the identical. + +You can see that the values in `app.json` are updated after running the command. + +#### Configuration Values + +:::info +When publishing your app, it is necessary to deploy Prose somewhere publicly accessible, perhaps on a cloud hosting provider like AWS or DigitalOcean. If Prose is only running on your local machine, users that download your app won't be able to use it. +Check [the documentation](../deployment.md) to deploy Prose if you haven't already. + +In the original release of Lexicon, the **Prose URL** was specified in `frontend/.env`. However, as part of migrating to Expo's EAS feature, we centralized the configuration into `frontend/Config.ts` to save you the trouble of needing to maintain it in more than one place, as suggested in the [Expo documentation](https://docs.expo.dev/build-reference/variables/#can-i-share-environment-variables-defined-in-easjson-with-expo-start-and-eas-update) +::: + +Next, open `Config.ts` and overwrite the placeholder values with the Prose URL you want to use for the build version. You can either set the same values or a different one for each channel. You don't need to adjust the values in `localDevelopment` since that is only used in development, and not when building the app. + +```ts +const config = { + // ... + buildChannels: { + preview: { + proseUrl: 'http://PLACEHOLDER.change.this.to.your.prose.url', + }, + production: { + proseUrl: 'http://PLACEHOLDER.change.this.to.your.prose.url', + }, + }, +}; +``` + +### Run a Build + +#### Build for Both Platforms + +To build on both platforms, you can use either of the commands below: + +```bash +eas build --platform all +``` + +```bash +eas build -p all +``` + +#### iOS only + +```bash +eas build --platform ios +``` + +#### Android only + +```bash +eas build --platform android +``` + +#### Run a build with a specific profile + +```bash +eas build --platform all –-profile +``` + +```bash +eas build -p all –e +``` + +:::note +Without --profile, the EAS CLI will default to the `production` profile. +::: + +### Build Profiles + +Build profiles serve as a way of grouping configuration values for different scenarios when building the mobile app. + +You can find more details [here](https://docs.expo.dev/build/eas-json/). + +The `eas.json` file can contain multiple build profiles. However, it typically has 3 profiles: **preview**, **development**, and **production**. + +#### 1. Preview + +Purpose: to internally test the app in production-like circumstances. + +It is recommended to try building with the preview profile **_first_** before building your app with the production profile. That way, you can ensure the app runs as expected before it’s ready to be published. + +The build type for Android will be an **APK** file, whereas the iOS build will output a format that can be installed on the simulator. + +This is because the `ios.simulator` option was specified in `eas.json`: + +```json + "ios": { + "simulator": true + }, +``` + +If you want to run the preview build on a real device, you'll need have an Apple account with Apple Developer Enterprise Program membership, then add the `ios.enterpriseProvisioning` value in `eas.json`: + +```json + "ios": { + "enterpriseProvisioning": "universal" + } +``` + +For the `preview` build profile, we have already set the distribution mode to [internal](https://docs.expo.dev/build/internal-distribution/). This ensures that EAS build provides shareable URLs for builds, with instructions on how to get them running. + +This approach then allows us to test the app without submitting to the App Store or Play Store. + +#### 2. Development + +Purpose: to make debugging easier. Expo will automatically include developer tools in the build. As you may have figured, this build should never be published to either of the app stores. + +Development builds depend on [expo-dev-client](https://docs.expo.dev/development/introduction/), so Expo will prompt us to install the library if needed. + +Similar to preview builds, you can add the iOS options mentioned above to run them on a simulator or real device. + +#### 3. Production + +Purpose: for submission to the App Store and Play Store—as a public release, or as part of testing in each respective ecosystem. + +In order to use builds like this, they must be installed through the respective app stores. + +After running builds with this profile, you'll see that the iOS and Android versions have automatically been incremented. As you might expect, this is because `autoIncrement` has been set to `true`. + +It is worth noting, however, that this behavior only applies to TestFlight and Internal Testing, so you'll need to be sure to also manually increment the `expo.version` in `app.json` for public release. Expo provides further [documentation](https://docs.expo.dev/build-reference/app-versions/) on this topic. + +## The App is Built + +Great work! You can now share the installation link with your peers so they can try out the app. + +In the next section, you'll learn how to [publish](publishing) your app to the App Store and Play Store! 🚀 diff --git a/documentation/versioned_docs/version-2.1.0/tutorial/install-prose.md b/documentation/versioned_docs/version-2.1.0/tutorial/install-prose.md new file mode 100644 index 00000000..d6ecec67 --- /dev/null +++ b/documentation/versioned_docs/version-2.1.0/tutorial/install-prose.md @@ -0,0 +1,360 @@ +--- +title: Setup the Prose GraphQL API +--- + +Now that we have a running Discourse instance to interact with, we can move onto setting up the Prose GraphQL Server. + +To recap, Prose is a part of the Lexicon stack. + +It is responsible for providing a [GraphQL](https://graphql.org/) interface on top of Discourse, which the Lexicon Mobile App can then interact with. + +For more information about this, check out [Concepts & Architecture](../concepts). + +## Approaches for Setting Up Prose + +If your Discourse instance is running locally, it is natural that you should also setup your Prose server locally. + +Otherwise, it would be unnecessary extra work to get a remote Prose server communicating with your local Discourse server. + +However, if you've setup your Discourse instance in the cloud, it is up to you if you want to run your Prose server locally or in the cloud as well. + +If you'd like to install it in the cloud, you'll want to setup an additional server - similar to how you would set one up for Discourse. If you're not yet comfortable with this, feel free to jump back to the page, [Setup a Cloud Server (Optional)](setup-cloud-server). + +Bearing all of that in mind, once you have identified where you'd like to host Prose, you should also consider how you'd like to install it onto that machine. + +The first way, which we recommend, is to use **[Docker](https://www.docker.com/)**. + +And of course, the second way is to install it manually, rather than using containers. + +## Install Prose using Docker + +The reason we recommend using Docker is because you won't have to worry about setting up Prose's on your machine. + +We have already published Prose to [Docker Hub](https://hub.docker.com/), which means you can easily pull it down and run it. We'll guide you through that below. + +### Install Docker + +First, just as was necessary for setting up Discourse, you'll want to make sure Docker is installed on your machine. + +You can follow the instructions on the [Docker installation page](https://www.docker.com/get-started) if you are unsure of how to do this. + +### Pull and Run the Prose GraphQL API Image + +After successfully installing Docker, you can use the command below to run the Prose GraphQL image. + +Just bear in mind that you'll want to adjust some of the **environment variables** to your situation before you run the command. + +``` +$ docker run -d \ + -e PROSE_DISCOURSE_HOST=https://meta.discourse.org \ + -e PROSE_APP_PORT=80 \ + -p 5000:80 \ + --name prose-graphql \ + kodefox/prose +``` + +The above command will take care of pulling the Prose GraphQL Docker Image, building it, and running it in a container. + +To help understand everything that's going on there, let's break it down line by line. + +```bash +docker run -d +``` + +This instructs Docker to run our image as a container in **detached mode**. This is similar to backgrounding a process. + +```bash +-e PROSE_DISCOURSE_HOST=https://meta.discourse.org +-e PROSE_APP_PORT=80 +``` + +The `-e` flag instructs Docker that we want to set or override certain environment variables in the container with the values we provided. + +In this case, we're telling Prose to interact with the Discourse instance is running at `https://meta.discourse.org`, and that Prose should run itself _inside of the container_ on a port of `80`. + +``` +-p 5000:80 +``` + +Next, we're telling Docker what ports we want to map from our host machine into the container. + +In the previous step, we established that Prose will run internally on port 80. With the above command, we're telling Docker to expose the container's port 80 as port 5000 on our host. + +This means that Prose will be reachable on port 5000 of the host. + +So, if you're running this locally, you'll be able to interact with Prose at `http://localhost:5000`. + +And if you're running it in the cloud on a domain like `https://prose.mydiscussions.com`, you'd likely want it to be listening on port 443 so the user doesn't have to enter a port number as part of the URL. + +### Configure Prose + +As suggested above, you can configure Prose through the use of environment variables. + +You can find a comprehensive list of all environment variables on the Prose [Environment Variables](../env-prose) page. + +In this case, you really only need to set a value for `PROSE_DISCOURSE_HOST`, which will instruct Prose which Discourse instance you'd like it to interact with. + +Additionally, if you'd like to set a different port mapping, you can adjust the `-p` flag of the `docker run` command to something else, such as: + +```bash +-p 8080:80 +``` + +## Install Manually + +This section, whether being done locally or remotely on a cloud provider, will require you to install and configure the necessary dependencies to build and run Prose from scratch. + +### Setup Development Machine + +If you haven't already, setup your machine for Prose development. You can do so by following the guide at [Setup your Development Machine](setup). + +By the time you're done with this step, you should have a local copy of the Lexicon repository on your desired machine. + +### Configure Environment Variables + +The Prose GraphQL API, at a bare minimum, requires you to provide a URL to an accessible Discourse instance in order to run properly. + +Because we're doing this manually, you'll need to specify this in a different way than you would for Docker. + +Later on, once you've built Prose, one way you can specify this is to simply provide it inline as you launch the server. + +```bash +PROSE_DISCOURSE_HOST=https://discourse.mysite.com node lib/index.js +``` + +However, you might find it more ergonomic to leverage the support we've setup for `.env` files. + +The entire Prose codebase lives in the `api/` directory of the repository, so get started by navigating there from the project root. + +``` +$ cd api/ +``` + +Next, you'll need to create a `.env` file. Simply copy the template file, `.env.example` into the `.env` file using the following command. + +``` +$ cp .env.example .env +``` + +After that, as you'd expect, you want to adjust the `.env` file so that it contains the values specific to your project. + +```bash +PROSE_DISCOURSE_HOST= +PROSE_APP_PORT= +``` + +As was covered in the Docker section above, you can find a comprehensive list of all environment variables on the Prose [Environment Variables](../env-prose) page. + +### Launch the Prose GraphQL API + +:::info +At this point, you should already have all the project's dependencies installed. + +If you encounter any errors about missing packages, go back to the guide at [Setup your Development Machine](setup). +::: + +If you'd just like to launch Prose to check it out quickly, you can simply run (from the `api/` directory): + +```bash +$ npm run dev +``` + +This will prepare and spin up Prose in a way that isn't ideal for production. + +If you wish to run the Prose GraphQL API in the background as a process, there are multiple solutions. + +One method is to use **[Tmux](https://github.com/tmux/tmux)**, which will detach the process from the terminal, allowing you to close it and keep Prose running. + +Another method is to use **[PM2](https://pm2.keymetrics.io/)**, which is a sophisticated toolset for running Node processes in production. + +#### Using Tmux + +**Tmux** can be used to detach processes from their controlling terminals, allowing sessions to remain active without being visible. + +To get started, install `tmux` on your machine. + +If you are unsure of how to install tmux, you can follow the instructions on [this page](https://github.com/tmux/tmux#installation). + +Once it's installed, launch it as follows: + +```bash +$ tmux +``` + +Then you can run Prose in the same way as before. + +```bash +$ npm run dev +``` + +If you want to detach from your current session, press `Ctrl + B` then press `d` on your keyboard. The session will remain active in the background. + +And if you wish to re-attach to your last session, run the following command. + +``` +$ tmux a +``` + +If you want to learn more about the tmux command, check out [this cheat sheet](https://tmuxcheatsheet.com/). + +#### Using PM2 + +Another way to run Prose in the background is to use **pm2** (process manager for NodeJS). + +First, as you'd expect, you'll need to install `pm2` on your machine. + +``` +$ npm install -g pm2 +``` + +Once it's installed, you'll also need to use `pm2` to install [Typescript](https://typescriptlang.org/). + +This is because Prose is written in Typescript, and this allows PM2 to run the Typescript files directly for us (as opposed to transpiling them and outputting them as JS first). + +To do this, simply run the following command: + +``` +$ pm2 install typescript +``` + +After that, you can now launch the Prose GraphQL API in the background with: + +``` +$ pm2 start src/index.ts +``` + +To list all running applications, run the following command. + +``` +$ pm2 list +``` + +These are some of the frequently used commands. + +``` +$ pm2 stop # To stop a process +$ pm2 restart # To restart a process +$ pm2 delete # To delete a process +``` + +## Test the GraphQL API + +Now that you've successfully launched Prose, you can actually interact with it in your web browser. + +Because of the libraries that we leveraged in building Prose, it automatically comes with [GraphiQL](https://www.graphql-yoga.com/docs/features/graphiql). + +This is an in-browser GraphQL IDE that makes it easy to explore the documentation and the schema of the GraphQL API. + +In order to access it, you'll need to make note of the host and port number that you configured the API with. + +For example, if you launched Prose from your local machine on port 5000, you'd navigate to [http://localhost:5000](http://localhost:5000). + +Similarly, if you set it up in the cloud, and all you have is an IP address with Prose listening on port 80, you would navigate to something like [http://174.31.92.1](http://174.31.92.1). + +Once the [GraphiQL](https://www.graphql-yoga.com/docs/features/graphiql) interface loads, you can test out some example queries and mutations, including logging into Discourse through Prose. + +### Login + +:::info +If you're accessing a private Discourse site, you'll need to make note of the token that is returned to make other requests. See below. +::: + +``` +mutation Login { + login(email: "user@lexicon.com", password: "user_password") { + ... on LoginOutput { + token + user { + id + name + username + avatarTemplate + } + } + } +} +``` + +As mentioned in the notice, if you're interacting with a private Discourse site, you'll need to provide a token for other GraphQL requests. + +As part of the response for the above mutation, you'll notice a "token" field which contains your authentication token in Base64. + +You use this token in other queries and mutations by opening the HTTP Headers section on the bottom left-hand side of the page. + +This section expects JSON, with which you'll want to add an Authorization header that contains your token. + +```json +{ + "Authorization": "" +} +``` + +Once you have done that, you can make authenticated GraphQL queries and mutations as the user you logged in with. + +### User Profile + +``` + query UserProfile { + userProfile(username: "john_doe") { + user { + ... on UserDetail { + id + avatarTemplate + username + name + websiteName + bioRaw + location + dateOfBirth + email + } + } + } + } +``` + +### Topic Detail + +``` +query TopicDetail { + topicDetail(topicId: 1) { + id + title + views + likeCount + postsCount + liked + categoryId + tags + createdAt + postStream { + posts { + id + topicId + userId + name + username + avatarTemplate + raw + createdAt + } + stream + } + details { + participants { + id + username + avatarTemplate + } + } + } +} +``` + +### Logout + +``` + mutation Logout { + logout (username: "john_doe") + } +``` diff --git a/documentation/versioned_docs/version-2.1.0/tutorial/intro.md b/documentation/versioned_docs/version-2.1.0/tutorial/intro.md new file mode 100644 index 00000000..0098344d --- /dev/null +++ b/documentation/versioned_docs/version-2.1.0/tutorial/intro.md @@ -0,0 +1,47 @@ +--- +title: Overview +slug: /tutorial +--- + +:::info +This tutorial **does not** cover the process of actually launching the app, as well as certain details about deploying to production. For support with those tasks, please refer to the documentation. +::: + +## Welcome to the Lexicon Tutorial + +We're really excited to help you dig in with the Lexicon Stack and learn how to deploy it in a way that benefits you and your users. + +## Target Audience & Prerequisites + +In order to complete this tutorial, you should have familiarity with: + +- The command-line +- Git and Github +- Setting up a Discourse instance +- Setting up servers in general + +In terms of prepararation, you will need: + +- NodeJS installed on your development machine + - Use the latest version of Node that is compatible with the project's version of Expo (i.e. `expo-cli`). +- An editor to edit config files + +#### Have some concerns? + +Interested in Lexicon but lacking in technical abilities? We completely understand. + +Reach out to us at support@kodefox.com to chat about how we can help bring your idea to life. + +## Next Steps + +This tutorial will guide you through the process of getting the entire Lexicon Stack up and running **locally** with your Discourse site. + +At the end of the tutorial, you will be able to interact with your Discourse site in the Lexicon Mobile App on your local device or simulator. + +You will also have an understanding of: + +- How to configure and run the Prose GraphQL API locally or on a server you own +- How to configure and run the Lexicon Mobile app on your device or in a simulator +- The next steps needed to make full use of Lexicon + +Let's get started! diff --git a/documentation/versioned_docs/version-2.1.0/tutorial/publishing.md b/documentation/versioned_docs/version-2.1.0/tutorial/publishing.md new file mode 100644 index 00000000..66005bdd --- /dev/null +++ b/documentation/versioned_docs/version-2.1.0/tutorial/publishing.md @@ -0,0 +1,97 @@ +--- +title: Publish your App +--- + +## EAS Submit + +EAS Submit is a service for uploading and submitting your application binaries to App Store and/or Play Store. +Check [here](https://docs.expo.dev/submit/introduction/) to learn more about EAS Submit. + +### Prerequisites: + +- Registered app in App Store Connect, see the guide [here](../app-store#register-a-new-bundle-id). +- Registered app in Play Store, see the guide [here](../play-store). + +### Configuration + +Before submitting, you are required to specify the credentials to publish your app. + +#### iOS + +For iOS, fill in your account information for `appleId`, `ascAppId`, and `appleTeamId`: + +```json + "base": { + "ios": { + "appleId": "", + "ascAppId": "", + "appleTeamId": "" + }, + ... + }, +``` + +- **appleId**: your apple ID (e.g., `john@gmail.com`). +- **ascAppId**: your App Store Connect app ID. Find your ascAppID by following [this guide](https://github.com/expo/fyi/blob/main/asc-app-id.md) (e.g., `1234567890`). +- **appleTeamId**: You can check your apple team ID [here](https://developer.apple.com/account/) (e.g., `12LE34XI45`). + +#### Android + +For Android, you will need to add a `.json` key file to authenticate with the Google Play Store. Please follow [this guide](https://github.com/expo/fyi/blob/main/creating-google-service-account.md) to generate one. Then, copy the JSON file to your `lexicon/frontend` directory, and rename the file as `playstore_secret.json`. + +The JSON file looks like this: + +```json +{ + "type": "service_account", + "project_id": "", + "private_key_id": "", + "private_key": "-----BEGIN PRIVATE KEY----------END PRIVATE KEY-----\n", + "client_email": "", + "client_id": "", + "auth_uri": "", + "token_uri": "", + "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", + "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/lexicon%40api.iam.gserviceaccount.com" +} +``` + +Now that the configuration is done, you can start submitting your app. + +### Submitting + +Use this command to submit the build: + +```bash +eas submit --platform ios --profile +``` + +Then you will see the EAS CLI prompt asking which app you would like to submit. + +There are 4 possible options: + +- Selecting a build from EAS +- Providing the URL of an app archive +- Providing the local path to an app binary file +- Providing the build ID of an existing build on EAS + +If you have built your app using EAS Build or have been following the tutorial from [Build your App](building), then please choose the first option, and select the version you want. + +### Submit Profiles + +By default, `eas.json` has been configured with two submit profiles, which are **staging** and **production**. + +The configuration is mostly the same, the only difference lies in the Android track options. + +- Staging infers the track as `internal`. This means submitting with the staging profile will submit the build for internal testing in the Play Store. +- Production infers the track as `production`, which will submit the build for Public Release in the Play Store. + +With iOS, on the other hand, both profiles will be submitted to TestFlight before you can release them publicly. + +You can reference the Expo documentation to learn more about [Android-specific](https://docs.expo.dev/submit/eas-json/#android-specific-options) and [iOS-specific](https://docs.expo.dev/submit/eas-json/#ios-specific-options) options. + +## Congratulations! + +Your app is now available for users to download from both the Play Store and the App Store! 🥳 + +To learn more about how to update your published app in the case of a bug, as well as OTA updates, check out the [next and final section](updating) of the tutorial. diff --git a/documentation/versioned_docs/version-2.1.0/tutorial/setup-cloud-server.md b/documentation/versioned_docs/version-2.1.0/tutorial/setup-cloud-server.md new file mode 100644 index 00000000..b69659a4 --- /dev/null +++ b/documentation/versioned_docs/version-2.1.0/tutorial/setup-cloud-server.md @@ -0,0 +1,27 @@ +--- +title: Setup a Cloud Server (Optional) +--- + +import useBaseUrl from '@docusaurus/useBaseUrl'; + +:::info +This is an optional section for users that don't feel as confident spinning up a new server with a cloud provider. + +If you are already adept at this, you can skip to the next section. +::: + +## DigitalOcean Guide + +### How To Set Up an Ubuntu 20.04 Server on a DigitalOcean Droplet + +For our users that aren't as familiar with setting up servers in the cloud, we wanted to provide you with a solid resource to learn more about it and accomplish something in the process. + +DigitalOcean has already provided an excellent guide to walk you through this, so we're going to link you over to them. + +In this guide, you will create an Ubuntu server through DigitalOcean’s administrative panel and configure it to work with your SSH keys. + +Once you have a solid understanding of how to setup servers in the cloud, you'll be much more capable of deploying the Lexicon Stack for your users. + +You can dig in on the article below. + +[Read: How To Set Up an Ubuntu 20.04 Server on a DigitalOcean Droplet](https://www.digitalocean.com/community/tutorials/how-to-set-up-an-ubuntu-20-04-server-on-a-digitalocean-droplet) diff --git a/documentation/versioned_docs/version-2.1.0/tutorial/setup-discourse.md b/documentation/versioned_docs/version-2.1.0/tutorial/setup-discourse.md new file mode 100644 index 00000000..9b5bf97a --- /dev/null +++ b/documentation/versioned_docs/version-2.1.0/tutorial/setup-discourse.md @@ -0,0 +1,306 @@ +--- +title: Prepare a Discourse Instance +--- + +Before you can properly setup Lexicon, you'll need to have a running **[Discourse](https://www.discourse.org/)** instance for Lexicon to interact with. + +For this step, you actually have a few options: + +#### Option 1: Setup a Local Discourse Instance + +The first option is to [setup a development instance](#setup-discourse-locally) of Discourse locally on your development machine. This takes a bit of time and can get a bit technical. + +#### Option 2: Buy a Discourse Instance or Use your Existing One + +The second option is to pay to [setup a Discourse instance in the cloud](#setup-discourse-in-the-cloud) as a live, reachable production verison. This is much simpler, but has the obvious tradeoff of costing money. + +And perhaps it goes without saying, but if you already have a Discourse site, feel free to just use that. + +#### Option 3: Use a Public Discourse Site + +The third option is to use an existing Discourse site just to test things out. + +As you'll see later on, Lexicon allows you to configure which Discourse site it is pointing at. As such, you can instruct it to point at at a publically accessible Discourse site that you don't personally own. + +There are countless examples of active Discourse communities out there. Here are a few examples to choose from: + +##### Discourse Meta + +[https://meta.discourse.org/](https://meta.discourse.org/) + +##### Expo + +[https://forums.expo.dev/](https://forums.expo.dev/) + +##### The Rust Programming Language + +[https://users.rust-lang.org/](https://users.rust-lang.org/) + +##### FreeCodeCamp Forums + +[https://forum.freecodecamp.org/](https://forum.freecodecamp.org/) + +## Setup Discourse Locally + +:::note +This section can take a long time. Depending on the specs of your machine, it could take between 10 - 30 minutes to complete. +::: + +This section of the tutorial is based on the following post on Discourse: [Beginners Guide to Install Discourse for Development using Docker](https://meta.discourse.org/t/beginners-guide-to-install-discourse-for-development-using-docker/102009). + +If you run into any issues, feel free to reference the original post and subsequent discussion. + +### Install Docker + +**[Docker](https://www.docker.com/)** is a containerization framework that makes it easy to build, manage, and deploy your application stack in a way that is safer, more reliable, and repeatable across multiple platforms. + +When developing, building, and testing applications locally, it is an invaluable tool that greatly simplifies the entire process. + +The main way that Docker helps us in this tutorial is that it won't require any modifications to our machine's environment other than installing Docker itself. + +This is as-opposed to needing to install all of Discourse's dependencies on your physical machine, in a way that may take a lot of effort to undo later. + +If you are unsure of how to install Docker, you can follow the instructions on their [website](https://www.docker.com/get-started). + +### Clone Discourse + +Once Docker is up and running, we can get started with setting up Discourse locally. + +The first step is to clone the Discourse repository to your local machine and `cd` into it. + +``` +git clone https://github.com/discourse/discourse.git +cd discourse +``` + +Note the repository is on the larger side (nearly 400mb), so this step may take a while depending on your connection. + +### Pull, Build, and Start the Discourse Dev Container + +:::caution +Make sure that the **host ports** listed below are not already in use on your device. +::: + +Discourse already contains a script to help spin up its entire infrastructure using Docker. + +During this process, the script will do the following: + +- Pull down the necessary "dev" Docker image to bootstrap Discourse +- Build the aforementioned image +- Run the image as a container with multiple ports mapped from your host into the container + - 127.0.0.1:**1080**->1080/tcp + - 127.0.0.1:**3000**->3000/tcp + - 127.0.0.1:**4200**->4200/tcp + - 127.0.0.1:**9292**->9292/tcp + - 127.0.0.1:**9405**->9405/tcp +- Prompt you for an admin email address and password + +To get started, simply run the following command: + +``` +$ d/boot_dev --init +``` + +Note that all of the Docker images add up to about 1GB of disk space usage on your device. + +The command will pause when it needs information from you. As shown below, it will prompt you for an administrator email address and password. + +```bash +# Output omitted +== 20200804144550 AddTitleToPolls: migrating ================================== +-- add_column(:polls, :title, :string) + -> 0.0014s +== 20200804144550 AddTitleToPolls: migrated (0.0021s) ========================= + +Creating admin user... +Email: me@me.com +Password: +Repeat password: + +Ensuring account is active! + +Account created successfully with username me +``` + +Next, it will ask you if you want to make this account an admin account. You do. + +```bash +Do you want to grant Admin privileges to this account? (Y/n) y + +Your account now has Admin privileges! +``` + +Please be aware, as suggested above, that the ports mentioned above are not currently in use by other processes. + +### If something unexpected happened + +It's possible that something strange may have happened at this step. + +Perhaps there was a weird error message, or the process just never displayed the output shown above. + +What we'd recommend doing is the following: + +#### Check if a Docker container named `discourse_dev` is running + +```bash +$ docker ps | grep discourse_dev +CONTAINER ID IMAGE ... NAMES +dc72a4ead10f discourse/discourse_dev:release ... discourse_dev +``` + +If it is, stop and remove the container. + +```bash +$ docker stop discourse_dev +discourse_dev +$ docker rm discourse_dev +discourse_dev +``` + +#### Exit or Kill the Existing Process + +If the existing process (`d/boot_dev --init`) is still occupying your terminal session, attempt to exit it via `Ctrl + C`. + +If the process is not responding to `Ctrl + C` after some time, locate its PID and use `kill -9` to kill it + +```bash +$ ps aux | grep rails +user 81254 0.0 0.1 discourse_dev bin/rails s + +$ kill -9 81254 +``` + +#### Restart Docker or your Machine + +Using the command or interface appropriate for your machine, you should restart all of Docker. + +On Docker for Mac, this is as simple as going into the tray icon and clicking Restart. + +#### Try running the command again + +Sometimes things just go a little haywire with this setup. Try running the command again to see if it works better this time. + +#### If you're absolutely stuck, reach out. + +Don't hestitate to contact us if you're just stuck with this one. + +### Optional: Run the Next Two Commands in the Background + +You can read on to get an understanding of what the two commands are, but it's worth mentioning that you want them to run simultaneously. + +You can do this by _backgrounding_ both processes. + +This means that they won't occupy your current session, requiring you to quit them in order to enter other commands. + +When you run this command, it will show you the process IDs (PIDs) of the processes that were backgrounded. + +To bring them back into the foreground, you can run the `fg` command, and then use `Ctrl + C` or a similar signal to stop them. + +```bash +d/rails s & d/ember-cli & +[2] 59786 +[3] 59787 + +fg +``` + +Just **note** that you won't see the output of the commands, and so you may need to be patient for several minutes until Discourse is reachable at its local address. + +Alternatively, you can use the PIDs to kill the processes outright in another session: + +```bash +kill -9 59786 59787 +``` + +### Start the Rails Server within the Container + +If you hadn't already noticed, Discourse is built in [Ruby](https://www.ruby-lang.org/en/) using the very popular web framework, [Ruby on Rails](https://rubyonrails.org/). + +By running the command below, you will be starting the Rails server, which will take some time, and will produce a tremendous amount of output. + +In particular, you'll see the database being initialized as the dev container bootstraps the Discourse server. + +To get started, simply run the following command. + +``` +d/rails s +``` + +#### If you later can't quit the process + +**Note** that this command can sometimes hang when you're trying to kill it with `Ctrl + C`. + +If that happens, it's recommended that you first stop the Docker container: + +```bash +docker stop docker_dev +``` + +Then, bring the process to the foreground with `fg` if necessary. + +Last, either exit your session if possible - such as by closing the Terminal - or find out the PID of the Rails process and kill it directly. + +```bash +$ ps aux | grep rails +user 81254 0.0 0.1 discourse_dev bin/rails s + +$ kill -9 81254 +``` + +### Run the Ember CLI + +The above section mentioned Ruby on Rails, which handles the backend aspects of the Discourse application. + +However, the Discourse frontend is build in [EmberJS](https://emberjs.com/), which is a batteries-included frontend web framework used by multiple major companies. + +Run the command below to instruct the Ember CLI to start the Discourse frontend. + +``` +d/ember-cli +``` + +Once you have done this, you'll be able to access Disourse at [http://localhost:4200](http://localhost:4200). + +Please note that the output of this command can be a bit confusing. And at times, it can seem like nothing is happening. + +You may see several progress indicators, as well as blank output, for several minutes before the server is ready. + +The output you're looking for will resemble the following: + +```bash +Build successful (72475ms) – Serving on http://localhost:4200/ + +Slowest Nodes (totalTime >= 5%) | Total (avg) +----------------------------------------------------------------------+------------------ +Babel: discourse (2) | 31501ms (15750 ms) +ember-auto-import-analyzer (11) | 10418ms (947 ms) +Bundler (1) | 6119ms +Babel: @ember/test-helpers (2) | 5075ms (2537 ms) +broccoli-persistent-filter:TemplateCompiler (3) | 4596ms (1532 ms) +``` + +## Setup Discourse in the Cloud + +There are several guides with instructions on how to setup Discourse in the Cloud. + +Rather than writing another one, we have found our favorite one and would like to send you over to them to give you a proper walkthrough of the process. + +### Guide by SSDNodes + +The guide is provided by the [SSDNodes](https://www.ssdnodes.com/?e=blog&q=more-about-ssdnodes) Blog, [Serverwise](https://blog.ssdnodes.com/blog/). + +If you aren't familiar, [SSDNodes](https://www.ssdnodes.com) is an excellent, cost-effective VPS hosting provider. + +While we are most familiar with Digital Ocean, we'd strongly encourage you to check them out as an alternative for hosting Discourse. + +The post, titled [How To Install Discourse On Ubuntu](https://blog.ssdnodes.com/blog/install-discourse/), is written by [Joel Hans](https://blog.ssdnodes.com/blog/author/joel/). + +Joel has written an excellent guide. He'll take you through the entire process, including making update to your Discourse instance. + +If you find yourself stuck, or have any questions, feel free to reach out to us. + +## Use a Public Discourse Site + +If you've chosen this option, there's not much to do other than to note the URL of the Discourse site you'll be using. + +Once you have that written down somewhere, you're ready for the next section. diff --git a/documentation/versioned_docs/version-2.1.0/tutorial/setup-mobile.md b/documentation/versioned_docs/version-2.1.0/tutorial/setup-mobile.md new file mode 100644 index 00000000..62ab943a --- /dev/null +++ b/documentation/versioned_docs/version-2.1.0/tutorial/setup-mobile.md @@ -0,0 +1,120 @@ +--- +title: Configure & Launch the Mobile App +--- + +import useBaseUrl from '@docusaurus/useBaseUrl'; + +After following the **[Setup the Prose GraphQL API](install-prose)** section, your GraphQL API should now be connected to your Discourse site. + +Next, we'll guide you through the process of connecting the Lexicon Mobile App to your Discourse site via Prose. + +### Mobile App Configuration + +:::note +In the original release of Lexicon, the **Prose URL** was specified in `frontend/.env`. However, as part of migrating to Expo's EAS feature, we centralized the configuration into `frontend/Config.ts` to save you the trouble of needing to maintain it in more than one place, as suggested in the [Expo documentation](https://docs.expo.dev/build-reference/variables/#can-i-share-environment-variables-defined-in-easjson-with-expo-start-and-eas-update) +::: + +Before launching your local version of the Lexicon Mobile App, you'll need to configure it with at least one piece of information. + +The Lexicon Mobile app relies exclusively on a running instance of the Prose GraphQL API in order to retrieve data from your Discourse instance. + +Therefore, you'll need to instruct it on how to locate your running Prose server. + +In development, it is common to have it running locally. However, if you have already deployed Prose +somewhere, feel free to use that. + +#### Configuring `proseUrl` via `config` + +:::caution + +##### `proseUrl` requirements + +It is worth noting that `proseUrl` **must** start with either `http://` or `https://`. + +If it does not, the Mobile App will throw an error when launching. +::: + +`Config.ts` contains the `config` object, which allows you to specify the Prose URL for each scenario encountered when developing and building the Mobile App. + +The specific configuration value which enables this is `proseUrl`, and it is contained within each scenario expressed by the `config` object. + +```ts +const config = { + localDevelopment: { + proseUrl: 'http://localhost:8929', + }, + buildChannels: { + preview: { + proseUrl: 'https://preview.myserver.com:8080/subpath', + }, + production: { + proseUrl: 'https://myserver.com/api/prose', + }, + }, +}; +``` + +As mentioned earlier—above, the `config` object allows us to express configuration values for multiple scenarios, which are: + +- `localDevelopment`: when developing against the app locally. This configuration is also used as a fallback for an unknown build channel. +- `buildChannels`: used to define configuration by build channel when building the app with the EAS CLI. + +`buildChannels` makes use of Expo's build channels (typically `preview` and `production`) as its keys. + +Each key within `buildChannels` maps to a specific Prose URL, which will be used for the build version based on which channel you build for. + +From the example above, when we create a `preview` build, the app will be built and configured to contact a Prose server located at `https://preview.myserver.com:8080/subpath`. + +The example above expresses a setup in which each build has its own deployed Prose server. However, it is also common to use one server for all scenarios, including development. + +```ts +const config = { + localDevelopment: { + proseUrl: 'https://myserver.com/api/prose', + }, + buildChannels: { + preview: { + proseUrl: 'https://myserver.com/api/prose', + }, + production: { + proseUrl: 'https://myserver.com/api/prose', + }, + }, +}; +``` + +##### Port Number + +Bear in mind that if your Prose server is not running on port 80 or 443, you also need to specify the **port number** via `proseUrl`. + +For example, if you've started a Prose server **locally** on port `8929` and try to run it using `expo start`, your `Config.ts` file would contain `http://myserver.com:8929/api/prose` under `localDevelopment.proseUrl`. + +### Launch the Mobile App + +Once you have configured everything, you'll want to launch the Mobile App to test that it is speaking to the right Prose server. + +To do this, you can simply run the following from the project root: + +```bash +npm run --prefix frontend start +``` + +The Expo development server should launch, and you can follow the instructions to run the app in a simulator or on your actual device. + +#### Troubleshooting + +If the app throws an error upon loading, you should double-check the configuration values you specified, according to the message you've received. + +If the app loads, but you're unable to actually connect, you should verify the following: + +- Your Prose Server is up and running at the location you provided to the Lexicon Mobile App +- Your Prose Server is configured to point at an accessible Discourse instance +- Your Discourse instance is up and running correctly + +## Nice Work! + +At this point, you've already accomplished a lot. + +The Discourse server you started off with is now accessible in a new way from a sleek native mobile app, and you're free to customize it to your heart's content. + +In the next part of the tutorial, we'll briefly get into that very topic: customizing the Mobile App to [white label](white-label) it for your brand. diff --git a/documentation/versioned_docs/version-2.1.0/tutorial/setup.md b/documentation/versioned_docs/version-2.1.0/tutorial/setup.md new file mode 100644 index 00000000..20505940 --- /dev/null +++ b/documentation/versioned_docs/version-2.1.0/tutorial/setup.md @@ -0,0 +1,98 @@ +--- +title: Setup your Development Machine +--- + +## Install NodeJS + +If you haven't already, install NodeJS on your machine. + +The tooling needed to setup Lexicon relies heavily on Node and npm. + +If you are unsure of how to install NodeJS, you can follow the instructions on the [NodeJS Website](https://nodejs.org/en/download/). + +#### Supported Node Versions + +It is recommended that you perform this tutorial using the latest version of Node that is compatible with the the project's version of Expo. + +You can always confirm this by viewing the dependencies in [frontend/package.json](https://github.com/lexiconhq/lexicon/blob/master/frontend/package.json). + +If your setup doesn't allow you to easily change your current Node version, we would recommend making use of [`nvm`](https://github.com/nvm-sh/nvm) to quickly switch between Node versions. + +### Install yarn, if you prefer + +Lexicon doesn't leverage any special features of [Yarn](https://yarnpkg.com/) - the alternative package manager for Node. If you prefer it, it will work the same as running `npm install`. + +For the purposes of this tutorial, we will demonstrate all commands using `npm`. + +### Clone the Lexicon Repository + +In a desirable location on your development machine, clone the Lexicon repository and `cd` into it. + +```sh +git clone git@github.com:lexiconhq/lexicon.git +cd lexicon +``` + +### Install Dependencies + +Next, install Lexicon's dependencies: + +```sh +npm install +``` + +This will install dependencies for both the Mobile App and the backend GraphQL API, Prose. + +### Install the Expo CLI + +[Expo](https://expo.io/) is the phenomenal toolchain that Lexicon uses to develop and build the Mobile App. + +We will later use the Expo CLI to launch the Mobile App - either on your device or in a simulator. + +You can install the Expo CLI with the following command: + +```sh +npm install --global expo-cli +``` + +Further information is available in the [Expo docs](https://docs.expo.io/). + +Then, verify that Expo is available in your `PATH` with the following: + +```sh +$ expo --version + +``` + +### Install the EAS CLI + +[Expo Application Services (EAS)](https://expo.dev/eas/) is an integrated set of cloud services for Expo and React Native apps. + +We will use the EAS CLI to build and publish the Mobile App. + +You can install the EAS CLI with the following command: + +```sh +npm install --global eas-cli +``` + +Further information is available in the [Expo docs](https://docs.expo.dev/eas/). + +Then, verify that EAS is available in your `PATH` with the following: + +```sh +$ eas --version +eas-cli/ +``` + +### Ready to Go! + +That's all we need for this step. + +Next, there is an optional guide to help you if you're not too familiar with setting up a server on a cloud provider. + +You're free to skip this if you're already adept at this process. + +After that, we'll look into how we can prepare Discourse to connect with the Lexicon Mobile App. + +If you don't already have a Discourse server setup, we'll get into that as well. diff --git a/documentation/versioned_docs/version-2.1.0/tutorial/updating.md b/documentation/versioned_docs/version-2.1.0/tutorial/updating.md new file mode 100644 index 00000000..46fcfdd1 --- /dev/null +++ b/documentation/versioned_docs/version-2.1.0/tutorial/updating.md @@ -0,0 +1,68 @@ +--- +title: Update your App +--- + +## EAS Update + +EAS Update is the successor to `expo publish`. This service helps to update projects using the `expo-updates` library. + +In particular, it enables you to push quick fixes to your users in between full-fledged app store submissions. + +With EAS Update, there is no need to recompile the app with its non-native parts, such as TypeScript code, styling, or image assets. [Click here](https://docs.expo.dev/eas-update/introduction/) to learn more about EAS Update. +:::note +You are required to build the app with [EAS Build](building) before using the EAS Update. +::: + +### Configuration + +Let's get started by configuring EAS update. Feel free to check out the [complete guide](https://docs.expo.dev/build-reference/build-configuration/) from Expo for further details. + +```bash +eas update:configure +``` + +Running this command will add `expo.updates.url` and `runtimeVersion.policy` in `app.json`. + +:::caution + +As mentioned in the [Expo documentation](https://docs.expo.dev/build/updates/#previewing-updates-in-development-builds), you can no longer launch your app in Expo Go (using `expo start`) after adding the `runtimeVersion` field in `app.json`. It is recommended to use `expo-dev-client` instead to create a development build. + +```bash +eas -p all -e development +``` + +or if you still wish to use Expo Go, please remove `runtimeVersion` field from `app.json` before running `expo start`. +::: + +### Updating + +After making the necessary changes, you can push updates using this command: + +```bash +eas update –-branch –-message “” +``` + +The branch name here is the same as the build profile name when building the app. +For example, if you had previously built the app with this command: + +```bash +eas build –p all –e preview +``` + +Then you can later update it using: + +```bash +eas update –-branch preview –-message “Fixing typos” +``` + +Once the update is complete, force close and reopen the installed app twice to view the update. + +## All Done! 🙌 + +That's it for the tutorial. Great work. + +We hope that this has served as an informative guide to help familiarize you with Lexicon and how you can make use of it. + +If you haven't already, check out the [Lexicon Documentation](../) to get a deeper understanding of the project and how it all works. + +If you have any questions, comments, feedback, or want to contribute, please reach out to us on Github! diff --git a/documentation/versioned_docs/version-2.1.0/tutorial/white-label.md b/documentation/versioned_docs/version-2.1.0/tutorial/white-label.md new file mode 100644 index 00000000..f0192667 --- /dev/null +++ b/documentation/versioned_docs/version-2.1.0/tutorial/white-label.md @@ -0,0 +1,82 @@ +--- +title: White Label your App +--- + +## Customize the Splash Screen and App Icon + +In order to customize the app for your own brand, you will likely want to provide your own assets for the **Splash Screen** and the **App Icon**. + +The **Splash Screen** - sometimes also referred to as the Launch Screen - is what appears while the app is launching. Some apps also display this to help conceal private information when the app is put into background mode. + +The **App Icon** is what is used to represent the app on the user's device, such as on the home screen and when listing it in the device's settings. + +Both of these assets often contain your logo in one form or another. For example, the App Icon for the Gmail app is the multi-colored outline of an envelope. Then, when launching the Gmail app, you will notice that the Splash Screen includes a larger version of the App Icon. + +### Customizing the Splash Screen + +:::info +Expo does not currently support dark mode for splash screens. +::: + +The assets used for the splash screen in the Mobile App are located at `frontend/assets/images/splash.png` and `frontend/assets/images/splashDark.png`. + +Above, we mention splash screen assets for both Dark Mode and Light Mode. + +However, unfortunately at this time, Expo does not support Dark Mode for Splash Screens. We have only included both so that they're ready when Expo finally does support this. + +In the meantime, you're free to adjust `splash.png` to influce what asset appears. + +In order to change it, you can simply replace the existing file with your own `splash.png`. + +To find out more about the Splash Screen image size and other details, please see the [Expo Splash Screen Guide](https://docs.expo.io/guides/splash-screens/). + +#### Futher Configuration + +To resize the Splash Screen image and change its background color, first open `frontend/app.json` and locate the `"splash"` field within it. + +As illustrated by the excerpt below, there are multiple fields that can be used to further adjust the Splash Screen: + +```json +"splash": { + "image": "./assets/splash.png", + "resizeMode": "contain", + "backgroundColor": "#FFFFFF" +}, +``` + +**image** + +The `image` field is fairly self-explanatory - it allows you to adjust what path will be used to locate the Splash Screen image. + +**resizeMode** + +The `resizeMode` field allows you to manage how the Splash Screen image will be resized to maintain its aspect ratio: + +- `contain` - Resize the image to make sure the whole image is visible. This is the default setting. +- `cover` - Resize the image to cover the entire container (in this case the whole screen) by either stretching or cropping the image as needed. + +Further details of how `contain` and `cover` behave are covered in the previously mentioned [Expo Splash Screen guide](https://docs.expo.io/guides/splash-screens/). For an even more detailed explanation, you can read [this post](http://blog.vjeux.com/2013/image/css-container-and-cover.html). + +**backgroundColor** + +The `backgroundColor` field enables you to specify the color of the background behind the Splash Screen image. Removing this value will result in usage of the default value, which is a white background color. + +### Customizing the App Icon + +Customizing the App Icon in Lexicon is nearly the same process as customizing the Splash Screen. + +The image asset for the Mobile App's icon is located at `frontend/assets/icon.png`. To customize it, simply overwrite that file with your own `icon.png`. + +## Further Customization + +We get into more detail about how to white label your app in the [White Labeling](../white-labeling) section of the documentation. + +In particular, this includes customizing and extending the theme's color palette, icons, and even fonts. + +Should you wish to customize anything not covered in that section, get in touch with us, and we'll see how we can help you make it a reality. + +## Awesome Work + +Your app looks cool now 😎. However, it's only accessible to you. + +Next, we'll cover how you can actually [build your app](building), so you can share it with the world. diff --git a/documentation/versioned_docs/version-2.1.0/white-labeling.md b/documentation/versioned_docs/version-2.1.0/white-labeling.md new file mode 100644 index 00000000..be413831 --- /dev/null +++ b/documentation/versioned_docs/version-2.1.0/white-labeling.md @@ -0,0 +1,13 @@ +--- +title: Overview +--- + +The Lexicon Mobile App allows you to customize its appearance through a process known as **White Labeling**. + +If you're unfamiliar with this term, it's essentially the process of branding an existing application specifically for your users. + +White Labeling allows you to configure the app with your own logo, app icon, color theme, fonts, and so on. + +The idea is that your users won't know that the Lexicon team built this app. Its appearance will be completely customized to your brand. + +To learn more about White Labeling the Lexicon Mobile App, continue to the next section. diff --git a/documentation/versioned_docs/version-2.2.0/activation-with-link/intro.md b/documentation/versioned_docs/version-2.2.0/activation-with-link/intro.md new file mode 100644 index 00000000..94aa9ce0 --- /dev/null +++ b/documentation/versioned_docs/version-2.2.0/activation-with-link/intro.md @@ -0,0 +1,9 @@ +--- +id: intro +title: Introduction +slug: discourse-plugin/activation-with-link +--- + +The Lexicon Discourse plugin provides support for integrating Discourse's email activation with your Lexicon-powered mobile app. Our plugin modifies links in specific Discourse activation emails account so that when a relevant link is tapped and the user has your Lexicon-powered mobile app installed, it will open the app and automatically activate account and log the user in. + +This section of the documentation offers step-by-step instructions to integrate activation with link into your Discourse site so that your users have a more seamless experience with your Lexicon-powered mobile app. diff --git a/documentation/versioned_docs/version-2.2.0/activation-with-link/setup/enable-activate-with-link.md b/documentation/versioned_docs/version-2.2.0/activation-with-link/setup/enable-activate-with-link.md new file mode 100644 index 00000000..6316850b --- /dev/null +++ b/documentation/versioned_docs/version-2.2.0/activation-with-link/setup/enable-activate-with-link.md @@ -0,0 +1,25 @@ +--- +title: Enabling activation account with link +--- + +import useBaseUrl from '@docusaurus/useBaseUrl'; + +This guide will walk you through the necessary steps to activate activation account with link at lexicon app on your Discourse site. + +## Steps + +1. Access your Discourse admin dashboard. + +2. Navigate to the `Plugins` section. + + + +3. Locate the `discourse-lexicon-plugin` and click on the `Settings` button. + +4. Fill in the `lexicon app scheme` setting with your app scheme. The app scheme is required to enable activation with link. + +5. Check the `lexicon activate account link enabled` box in the Lexicon settings section and save your changes. + + + +Once the activation account with link feature is enabled, you will be able to utilize its functionality in your Discourse instance. diff --git a/documentation/versioned_docs/version-2.2.0/activation-with-link/setup/verify-activate-with-link.md b/documentation/versioned_docs/version-2.2.0/activation-with-link/setup/verify-activate-with-link.md new file mode 100644 index 00000000..d8d6c42f --- /dev/null +++ b/documentation/versioned_docs/version-2.2.0/activation-with-link/setup/verify-activate-with-link.md @@ -0,0 +1,55 @@ +--- +title: Verify Activation Account With Link +--- + +import useBaseUrl from '@docusaurus/useBaseUrl'; + +Below, we'll walk you through how you can validate the functionality of activation account with a link within your Lexicon-powered mobile app. + +:::note +The steps below assume that **you have already build your Lexicon-powered mobile app with the correct app scheme**. If you are running the app on your machine locally through Expo, these steps will not work. + +In order to test account activation with a link, **you will need to use Lexicon version 2.2.0** for your Lexicon app. This feature only works if the user signs up by themselves, not through an invitation from an admin or moderator. Therefore, it is required to disable the **invite only** setting in the Discourse admin settings. + +::: + +:::info +To be able to test this feature, you need an email account that has not been registered in Discourse. + +You also need to be able to log in as an admin on the Discourse website in case you need to approve new users. This is necessary if you have enabled the `must approve users` setting in the Discourse admin settings. + +::: + +## Steps + +To test activation account with link within your Lexicon-powered mobile app, follow these steps: + +1. Ensure that you have Lexicon-powered mobile app at your device. +2. On your mobile device, open your Lexicon-powered mobile app and sign up using new email or you can sign up from discourse website. + > **Note**: + > + > - Ensure that your email client on your mobile device will receive emails for this account. + > - If you want to sign up using mobile app disable Discourse's setting `login required` + +
+ + +
+ +3. After you finish sign up you will receive email to activate account. +4. Open your email on your phone and check the email sent by your Discourse website. + +
+ +
+ +5. Click the link provided in the email. + +
+ +
+ +6. The link will first open in your mobile web browser. When you click `Open App`, if the Lexicon-powered mobile app is installed and matches the configured app scheme, it should automatically open your app and attempt to log you in. + > **Note:** If your admin settings require user approval, the login will fail, and a popup will appear indicating that a moderator's approval is required. + +And that's it! The Lexicon Discourse plugin will properly log you in with a link through your Discourse site. diff --git a/documentation/versioned_docs/version-2.2.0/app-store.md b/documentation/versioned_docs/version-2.2.0/app-store.md new file mode 100644 index 00000000..f91b5fa0 --- /dev/null +++ b/documentation/versioned_docs/version-2.2.0/app-store.md @@ -0,0 +1,282 @@ +--- +title: Publishing to the App Store +--- + +import useBaseUrl from '@docusaurus/useBaseUrl'; + +At this point, you've at least made some minor adjustments to the Lexicon Mobile App, and are ready to publish it so that your users can download it. + +In this page, we'll cover the process of publishing it on iOS. + +## Prerequisites + +- An Apple Developer account +- An Expo account +- XCode is installed on your development machine +- EAS CLI 2.6.0 or newer +- The [Lexicon Discourse plugin](./discourse-plugin.md) is already installed on your Discourse instance + +To get started with TestFlight and publishing your app, you'll need an **Apple Developer account**. + +This will enable you to interact with Apple as you go through the process of submitting to TestFlight and, eventually, the App Store. + +You'll also need an [Expo account](https://expo.dev/signup) so you can build your app, download it, and upload it to Apple's servers. + +Finally, you'll want to have already downloaded and installed [Xcode](https://developer.apple.com/xcode/), which is what you'll use to upload your built app to Apple's servers. + +:::note +If you don't yet have an account with Apple, you'll need to enroll in the [Apple Developer Program](https://developer.apple.com/programs/enroll/) first. Note that there is an annual cost associated with this. + +Additionally, you'll want to make sure you have an account with [Expo](https://expo.dev/signup) so you can use features like [EAS Submit](https://docs.expo.dev/submit/introduction/). +::: + +## Register a new Bundle ID + +Each app in Apple's App Store has a unique **Bundle Identifier**, or Bundle ID. + +In order to publish the app anywhere, including to TestFlight, you'll need to have a Bundle ID registered for your app with Apple. + +Typically, this uses the format of `com..`. + +For example, if your company is named Expo, and your app is named Expo Go, your Bundle ID could be: + +``` +com.expo.expogo +``` + +You can follow these instructions to get one. + +- Go to [Certificates, Identifiers & Profiles](https://developer.apple.com/account/resources/identifiers/bundleId/add/bundle). +- Fill in the following fields, and then click `Continue` + Regsiter App + + - **Description**: You can insert the app name as its description. + + - **Bundle ID**: Select `Explicit`, and then insert then insert your bundle ID in the input field. + +- Capabilities + + - You can leave this section empty. + +## Add a New App in App Store Connect + +Steps: + +- Sign in to your [App Store Connect](https://appstoreconnect.apple.com/) account. +- Click on `My Apps`. + App Connect +- Click on the `+` button to add new app. + Add New App +- Fill out the requested information about your app, and then click `Create`. + Add New App + + - **Platforms**: Select `iOS`. + - **Name**: The name of your app, as it will appear on the App Store and user's devices. + - **Primary Language**: The primary language that will be used if localized app information is not available. + - **Bundle ID**: Choose the Bundle ID you created above. + - **Note**: double-check that it's correct, because you can not change it afterwards. + - **SKU (Stock Keeping Unit)**: A unique ID to differentiate your app from the others, similar to a product ID. + - **User Access**: Full access means all users will have access to the app, while limited access means that the app can only be accessed by certain roles defined within App Store Connect. + +## Configuration + +After creating the app in App Store Connect, you'll want to jump back over to the codebase and make some adjustments. + +### Build Config + +:::note +If you haven't yet installed the EAS CLI, follow the instructions in the [tutorial](tutorial/setup#install-the-eas-cli). +::: + +First, you'll need to ensure you've set your app name and slug in `frontend/app.json`. The [slug](https://docs.expo.dev/workflow/glossary-of-terms/#slug) is used as part of the URL for your app on Expo's web services, so it is recommended to use kebab-case (e.g., `my-lexicon-app`). + +Replace these placeholders with your desired values: + +:::info +Note below that `scheme` is included. If you want [email deep linking](./email-deep-linking/intro.md) support in your app, **you must specify a scheme**, and then configure the Lexicon Discourse plugin with the same scheme. +::: + +```json +"name": "", +"slug": "", +"scheme": "", +``` + +Next, configure EAS Build by running this command from the `frontend/` directory: + +```bash +eas build:configure +``` + +The EAS CLI will prompt you to specify `android.package` and `ios.bundleIdentifier` if those values are not already provided in `app.json`. You'll want to add the bundle ID you just registered in App Store Connect as the `bundleIdentifier`. + +Then you can see that the value has been updated in the `ios` section of `frontend/app.json` file. + +```json + "ios": { + "supportsTablet": false, + "buildNumber": "1.0.0", + "bundleIdentifier": "", + "config": { + "usesNonExemptEncryption" : false + } + }, +``` + +:::note +We set `usesNonExemptEncryption` to `false` because Lexicon doesn't leverage that feature. + +For further details, please take a look at [this link](https://developer.apple.com/documentation/bundleresources/information_property_list/itsappusesnonexemptencryption) from Apple's documentation. +::: + +### Setup Config Values + +:::info +When publishing your app, it is necessary to deploy Prose somewhere publicly accessible, perhaps on a cloud hosting provider like AWS or DigitalOcean. If Prose is only running on your local machine, users that download your app won't be able to use it. +Check [the documentation](deployment) to deploy Prose if you haven't already. +::: + +Next, configure the **Prose URL** for your build in `Config.ts`. You can set a different URL for each build channel. + +:::note +In the original release of Lexicon, the **Prose URL** was specified in `frontend/.env`. However, as part of migrating to Expo's EAS feature, we centralized the configuration into `frontend/Config.ts` to save you the trouble of needing to maintain it in more than one place, as suggested in the [Expo documentation](https://docs.expo.dev/build-reference/variables/#can-i-share-environment-variables-defined-in-easjson-with-expo-start-and-eas-update) +::: + +```ts +const config = { + // ... + buildChannels: { + preview: { + proseUrl: 'http://PLACEHOLDER.change.this.to.your.prose.url', + }, + production: { + proseUrl: 'http://PLACEHOLDER.change.this.to.your.prose.url', + }, + }, +}; +``` + +### Setup Apple Dveloper Account + +Lastly, please adjust these fields in `eas.json` with your account information to submit the app: + +```json + "base": { + "ios": { + "appleId": "", + "ascAppId": "", + "appleTeamId": "" + }, + ... + }, +``` + +- **appleId**: your apple ID (e.g., `john@gmail.com`). +- **ascAppId**: your App Store Connect app ID. Find your ascAppID by following [this guide](https://github.com/expo/fyi/blob/main/asc-app-id.md) (e.g., `1234567890`). +- **appleTeamId**: You can check your apple team ID [here](https://developer.apple.com/account/) (e.g., `12LE34XI45`). + +## Build your App for iOS + +Before publishing, you'll need to build your app by instructing Expo to generate an iOS build. + +It is recommended to build your app with the `preview` profile before releasing to verify that it works as expected. See [this tutorial](tutorial/building) to learn more about build profiles. + +Run this command: + +```bash +eas build --platform ios --profile preview +``` + +When you run the above command, Expo will prompt you for your Apple ID and password. + +Once the above step has been completed, login to your account on [Expo](https://expo.dev) and download your newly built app. + +Navigate to your project in the [Expo web console](https://expo.dev), then click on the **Builds** menu located on the left-hand side of the screen. + +- Click on the project you want to install. + Builds + +- Download the iOS build by pressing the `Download` button in the `Build Artifact` section. + Build Artifact + +This will download a tar file containing your app. Extract the file, then drag it to your simulator to install it. See [this section](tutorial/building#1-preview) of the tutorial to learn about running the app on real devices. + +Once you have verified that the app runs as expected, you can proceed to build it for release: + +```bash +eas build --platform ios --profile production +``` + +The approach for a production build is similar to the one used for generating a preview build. However, unlike a preview build, you won't be able to launch the production build in the iOS simulator—it is intended solely for publishing to the App Store. + +Once this process is completed, you can proceed with submitting it to Apple. This process typically involves Apple's TestFlight service. + +## Submit to TestFlight + +TestFlight is a key aspect of Apple's Developer Program, which enables developers to provide beta users with access to their app under less restrictive review requirements. + +With TestFlight, you're able to invite users to test your app and collect their feedback before releasing it to the public on the App Store. You can learn more about TestFlight [here](https://developer.apple.com/testflight/). + +Submitting an iOS app is much easier with EAS Submit. This is covered in more detail in the [tutorial](tutorial/publishing). + +Run the following command to start publishing the app to TestFlight: + +```bash +eas submit --platform ios --profile production +``` + +Once the process has completed successfully, we can check the build in App Store Connect. + +In App Store Connect, click on the TestFlight Tab. + +You'll see the [status](https://help.apple.com/app-store-connect/#/dev3d6869aff) of your built version. + +- **Red** indicates that you need to perform some action. +- **Yellow** indicates that some aspect of the process is pending—either from you, or from Apple. +- **Green** indicates that the build is being tested in TestFlight, or is ready to be submitted for review. + +You won't be able to begin beta testing with TestFlight until an official tester from Apple verifies your app. + +In order to allow Apple to properly test your Lexicon-powered app, they'll need to have credentials to login your Discourse site. + +Before submitting your app, you'll need to create those credentials in Discourse and specify them in App Store Connect. + +- In App Store Connect, click on your app. +- Click on TestFlight App. +- Click on Test Information in the sidebar on the left-hand side. +- Fill the required fields, then check the `Sign in required` checkbox, and enter the credentials. + Review Information Sign In +- Please also provide information for a person to contact if the review team needs additional information. + Review Information Contact + +### Specify Users for Beta Testing + +Beta Test Users can belong to an Internal Group or an External Group. + +You can specify internal users by going to the Internal Group section, and clicking on **App Store Connect Users**. + +Similarly, you can specify external users by selecting External Groups, and clicking on **Add External Testers**. + +#### More Information + +TestFlight and App Store Connect are sophisticated tools to help with the process of submitting, testing, and publishing your app. + +If you have further questions or just want to learn more, we'd recommend that you make use of Apple's documentation, which is very high quality. + +For more information about TestFlight in general, read the [documentation](https://developer.apple.com/testflight/). + +Similarly, for specific information about beta testing with TestFlight, check out [Testing Apps with TestFlight](https://testflight.apple.com/). + +## Publish to the App Store + +Once you've successfully passed Apple's review process and have received enough feedback from your beta testers, you're ready to publish to the App Store and go live! :tada: + +As a few final reminders, double-check that... + +- Your Discourse instance is online, reachable, and functioning correctly. +- The built version of your app is configured to point at the correct Prose server. +- Your Prose server is online, reachable and healthy. +- Your Prose server is deployed with the [recommended guidlines](dedicated#configure--deploy-prose) for production. + - In particular, ensure that its traffic is encrypted using an SSL certificate. + +Next, we'll guide you through the process of publishing your app for Android devices on the Google Play Store. diff --git a/documentation/versioned_docs/version-2.2.0/assets.md b/documentation/versioned_docs/version-2.2.0/assets.md new file mode 100644 index 00000000..a24dcad0 --- /dev/null +++ b/documentation/versioned_docs/version-2.2.0/assets.md @@ -0,0 +1,47 @@ +--- +title: App Icon & Other Assets +--- + +The Lexicon Mobile App contains multiple assets that can be replaced in order to White Label it. + +The assets that can be modified are as follows: + +## App Logo + +Used to show the app logo in the application, such as on the Login, Register, and 2FA Scenes. + +The assets are located at `frontend/assets/images/logo.png` and `frontend/assets/images/logoDark.png`. The `logo.png` is used in light color scheme and `logoDark.png` is used in dark color scheme. To customize it, simply replace the existing file with your own `logo.png` and `logoDark.png`. + +## Favicon + +Used to show the app logo. + +The asset is located at `frontend/assets/favicon.png`. To customize it, simply replace the existing file with your own `favicon.png`. + +## Image Placeholder + +Used to temporarily take an image place when it is loading. + +The asset is located at `frontend/assets/images/imagePlaceholder.png`. To customize it, simply replace the existing file with your own `imagePlaceholder.png`. + +## Icons + +Used to display icons inside the application. + +The assets are located in the `frontend/assets/icons` folder. If you want to add more or edit the remaining icons, you need to insert the icons to the `frontend/assets/icons/` folder and import them in `frontend/src/icons.ts`. + +There are some standards applied to the icons, such as: + +#### Uniform Icon Size to Maintain Visual Consistency + +The UI is designed around the default base dimensions of 28x28px for icons. + +If you adjust this, you may need to modify other aspects of the theme or fonts in order to maintain a clean appearance. + +Similarly, if you provide a new icon that does not conform to these dimensions, you may run into visual inconsistencies. + +#### SVG Icons have their Fill Color Controlled via `currentColor` + +If you are adding a new icon that you expect to interact with theme's colors, ensure that its color is not hard-coded, and is instead set to `currentColor`. + +If you are unfamiliar with this concept, take a look at the [MDN Specification on SVG color values](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/color). diff --git a/documentation/versioned_docs/version-2.2.0/commercial-support.md b/documentation/versioned_docs/version-2.2.0/commercial-support.md new file mode 100644 index 00000000..9d164810 --- /dev/null +++ b/documentation/versioned_docs/version-2.2.0/commercial-support.md @@ -0,0 +1,9 @@ +--- +title: Commercial Support +--- + +With official support, you get expert help straight from the core team. We provide app customization, dedicated support, prioritize feature requests, deployment strategies, advice on best practices, design decisions, and team augmentation. + +Additionally, we are open to engagements for non-technical site owners looking to customize, deploy, and launch a mobile app for their Discourse users. + +Reach out to us for consulting at support@kodefox.com. diff --git a/documentation/versioned_docs/version-2.2.0/concepts.md b/documentation/versioned_docs/version-2.2.0/concepts.md new file mode 100644 index 00000000..44211e43 --- /dev/null +++ b/documentation/versioned_docs/version-2.2.0/concepts.md @@ -0,0 +1,68 @@ +--- +title: Concepts and Architecture +--- + +import useBaseUrl from '@docusaurus/useBaseUrl'; + +## Prose: Discourse through GraphQL + +It is worth acknowledging upfront that Discourse already provides a traditional, RESTful API for developers out of the box. + +However, [the official documentation](https://docs.discourse.org/) for this API points out that it is incomplete, effectively serving as a starting point. + +> Note: For any endpoints not listed you can follow the reverse engineer the Discourse API guide to figure out how to use an API endpoint. +> +> —**Discourse API Documentation** + +The core team, as well as members of the [support forum](https://meta.discourse.org), regularly respond to questions about the API by [encouraging developers to reverse-engineer the API](https://meta.discourse.org/t/how-to-reverse-engineer-the-discourse-api/20576). As of this writing, the topic for how to reverse engineer the API has been linked to from nearly 200 other topics on the support forum. + +To help you simplify the process for you, Prose strives to normalize a subset of the API. We have done so with the hope that it will save you some time as you develop against Discourse. + +#### GraphiQL + +Prose's GraphQL implementation includes an [in-browser GraphQL IDE](https://www.graphql-yoga.com/docs/features/graphiql), known as [GraphiQL](https://github.com/graphql/graphiql), which allows developers to easily reference the entire documentation and schema and make queries against a running Discourse instance. + + + +This means you can rapidly get a clear understanding of how a method behaves—and what parameters it requires—without digging through support posts or reverse-engineering the REST API. + +#### Why GraphQL? + +There is no shortage of articles about both the [benefits](https://www.howtographql.com/basics/1-graphql-is-the-better-rest) and [tradeoffs](https://lwhorton.github.io/2019/08/24/graphql-tradeoffs.html) of GraphQL. + +We're well aware that GraphQL isn't some magical solution that solves all the problems of other API paradigms. + +Having said that, we chose to build Lexicon with it for two primary reasons. + +1. Our team is familiar and fluent with GraphQL, and deeply enjoys working with it. + +2. The tooling, libraries, and auto-generated documentation provide out-of-the box benefits which we can pass onto others with no additional effort. + +#### Why Expo? + +[Expo](https://docs.expo.io/) is both a framework and a platform for building universal React applications. In particular, it provides a superior development experience when building mobile apps with React Native. + +We find that Expo makes us much more effective as developers, and also provides excellent services to facilitate the entire process of building and publishing React Native apps. + +In particular, Discourse sites that leverage the [Lexicon Discourse Plugin](./discourse-plugin.md) get the benefit of [push notifications](./push-notifications) through Expo's [push notifications service](https://docs.expo.dev/push-notifications/overview/), which abstracts away Google and Apple's push services into a simple interface. + +## Lexicon Architecture + +The Lexicon Stack is fairly simple, and only consists of 3 major pieces: + +- The Lexicon Mobile App +- The Prose GraphQL API +- A running, accessible Discourse instance +- Optionally, you can install our [Discourse Plugin](./discourse-plugin.md) to enable additional features. + +Below is a diagram illustrating the typical architecture for a Lexicon-powered mobile app. + +IOS Lexicon Login Page + +As indicated above, the mobile app makes requests to a deployed Prose GraphQL server. + +The Prose server has been configured to point at an active Discourse instance of the developer's choice. + +If the [Lexicon Discourse Plugin](./discourse-plugin.md) is installed, additional endpoints will be exposed which Prose already knows how to communicate with. + +Traffic then flows back from Discourse, through Prose, and returns to the mobile app over a GraphQL interface. diff --git a/documentation/versioned_docs/version-2.2.0/contributing.md b/documentation/versioned_docs/version-2.2.0/contributing.md new file mode 100644 index 00000000..0fe6fbe9 --- /dev/null +++ b/documentation/versioned_docs/version-2.2.0/contributing.md @@ -0,0 +1,131 @@ +--- +title: Contributing +--- + +Thank you for your interest in contributing! :sparkles: + +We greatly appreciate the time and effort you're willing to put forth to make Lexicon even better. + +There are several ways to help out. + +## Reporting Bugs + +The best way to let us know about a bug is by [creating a new issue](https://github.com/lexiconhq/lexicon/issues/new) on [Github](https://github.com/lexiconhq/lexicon). + +As always, we recommend searching the existing open and closed issues before opening a new one. + +When you create the issue, please be sure to include the following: + +- A detailed description of the bug and its behavior + +- The behavior you expected instead of the bug + +- A list of steps for how to reproduce the bug + +- Details about the device(s) and version(s) you're observing the bug on + +- Screenshots and screen recordings, while not necessary, are very welcome! + +Once we've received your bug report, we will triage it and label it accordingly. + +## Contribute to the Project + +Want the honor of being listed in our contributors section :clap:? + +We'd love to get a PR from you addressing an existing issue, adding a feature, or even just improving the documentation. + +To get started contributing, follow the instructions below. + +### Instructions + +**1. Fork the [official Lexicon repository](https://github.com/lexiconhq/lexicon)** + +You probably already know the drill - click on **Fork** button on the upper-right corner. + +**2. Clone your Fork of Lexicon** + +Be sure to clone **_your_** fork to your development machine (as opposed to cloning the main Lexicon repository). + +``` +$ git clone https://github.com/YOUR_USERNAME/lexicon.git +``` + +If you need further guidance with cloning, head over to our [Quick Start](quick-start#installation) section. + +Just bear in mind that the Quick Start section walks you through cloning the Lexicon repository. So make sure you change the URL to your username as referenced above. + +**3. Run and connect the app with Prose and a Discourse Host** + +For a comprehensive walk-through of this step, follow the instructions in the [**Setup**](setup#discourse-host) section. + +If you already have a deployed Prose instance that is pointing at a Discourse instance, you can simply configure the Lexicon Mobile App to point at the address of your Prose deployment. + +However, if you don't have that, or if you're planning on making adjustments to the Prose server itself, you'll want to ensure the Lexicon Mobile App is configured to point at a Prose server that you have running locally. + +**4. Get Started with your Contribution** + +At this point, you should be setup to dig in on the main work of your feature, bugfix, or other contribution. + +Remember that it's necessary to have the [**ESLint**](https://eslint.org/docs/user-guide/getting-started) and [**Prettier**](https://prettier.io/) plugins installed in your IDE, as those are required in order for the Pull Request checks to pass. + +We would recommend working in [VSCode](https://code.visualstudio.com/), since that is what we used to develop Lexicon. However, it is up to you, you only need to ensure that ESLint and Prettier are functioning properly within your IDE. + +**5. Run the Test Suite** + +Follow these [**steps**](setup#run-the-test-suite) to run the Lexicon test suite. + +In order to speed up the feedback cycle, it is recommended that you ensure that all tests are passing locally before pushing, especially if you already have an open PR. + +This is primarily because we have configured our Github project to block PRs from being merged if any of the build steps fail. + +If the reviewers see that tests are failing, they aren't able to review it as quickly, and will likely request that you resolve any build issues before requesting review again. + +**6. Stage, Commit, and Push your Local Changes** + +If you're unfamiliar with this process, please take a look at this [great article](https://github.com/git-guides/#learning--mastering-git-commands) from Github to bring you up to speed. + +**7. Create a New Pull Request** + +Your code is ready to submit! :tada: + +Go to the Lexicon [Pull Requests tab](https://github.com/lexiconhq/lexicon/pulls), and compare the changes between your branch and the master branch. + +Double-check and make sure you didn't push anything you don't want included in your PR. + +Then, go ahead and create a new Pull Request from your forked repository. + +Please be sure to follow the Pull Request template, add related labels, and please mention the issue you are addressing to help us keep track of what's being worked on. + +## Share Your Thoughts with Us + +We'd love to hear your new ideas! Drop them in the [Issues tab](https://github.com/lexiconhq/lexicon/issues). + +## Spread the Word + +Let others know about your awesome experience using Lexicon on social media, and tag us on Twitter [@GetLexicon](https://twitter.com/GetLexicon). + +And if you build your app using Lexicon, please let us know. We'd love to help you spread the word about what you've built! + +## Improve the Documentation + +As a closing thought, if you find any issues with the Lexicon documentation, or just think you could make it better, you can get started with these brief instructions below. + +To generate and run the documentation locally, from the project root, run: + +```sh +npm run docs:start +``` + +Similarly, you can build the documentation using: + +```sh +npm run docs:build +``` + +All documentation is in the `documentation/` directory, and the Markdown pages used to generate this site are under `documentation/docs`. + +If you end up making a PR to improve the documentation, please be sure to label your PR with the `Documentation` label. + +:::note +Don't hesitate to ask if you have any further questions. We're always happy to help. :smile: +::: diff --git a/documentation/versioned_docs/version-2.2.0/customize.md b/documentation/versioned_docs/version-2.2.0/customize.md new file mode 100644 index 00000000..6797d756 --- /dev/null +++ b/documentation/versioned_docs/version-2.2.0/customize.md @@ -0,0 +1,28 @@ +--- +title: Customization +--- + +## Theming + +As part of its [White Labeling Support](white-labeling), Lexicon allows you to customize the theme of the mobile app. +You can configure the base and functional colors according to a color scheme of your choosing. +You're also able to customize icons, fonts, and even the error messages that appear inside of the mobile app. +To get started with this, check out the [Theming page](theming) under the [White Labeling](white-labeling) section. + +## White Labeling the Mobile App Assets + +To provide your users with a unique experience that matches your brand, you can customize the splash screen and app icon on their device. + +This will replace all Lexicon branding with your own. + +Further details can be found in both the [Tutorial](tutorial/white-label) and the [White Labeling Section](white-labeling) of this documentation. + +## Enabling Additional Discourse features + +As you might already be aware, Discourse is a highly customizable piece of software. Much of it is customizable from the Admin Site Settings page on your Discourse instance. + +Some of these settings will translate automatically into the Lexicon Mobile App, such as `authorized extensions`. + +In general, we have done our best to get out of the way and use Discourse as the source of truth for how the Lexicon Mobile App should appear and behave. + +If you find any settings that Lexicon is not responding to, but you feel it should, please open an issue and let us know. diff --git a/documentation/versioned_docs/version-2.2.0/dedicated.md b/documentation/versioned_docs/version-2.2.0/dedicated.md new file mode 100644 index 00000000..75fe8afe --- /dev/null +++ b/documentation/versioned_docs/version-2.2.0/dedicated.md @@ -0,0 +1,300 @@ +--- +title: Hosting & Configuration +--- + +As mentioned in the [Overview](deployment), this section is meant to guide you through configuring and deploying Prose on a dedicated instance. + +## Decide on Where to Host + +First, you'll need to answer the following question. Where would you like to host Prose? + +While there are many options that vary by project and developer preferences, the simplest way is often to use a cloud provider of your choice. + +In the [Lexicon tutorial](tutorial/setup-cloud-server), we walk you through this process using Digital Ocean. + +If you're confused about this step, or don't have a preference, you should take some time to work through it. + +However, if you already know what you're doing, feel free to use any cloud provider or hosting solution of your choice. + +### Hosting Checklist + +Once you've decided on a host, go through the checklist below to verify that everything is setup as expected. + +#### ✅ Ensure Access & Permissions on the Host + +At a minimum, you will need to be able to login to the host. Some cloud providers offer a virtual, web-based terminal, but ideally you can get credentials to login directly. + +If your host is in a UNIX-based environment, you should also have permissions to run commands as `sudo`. + +A quick way to check this is to simply attempt to run a command with `sudo`: + +```sh +$ sudo ls +``` + +However, if you have a restrictive hosting environment, you will just need a way to place the Lexicon source onto the host, install its dependencies, and expose it on a port. + +Bear in mind that a restrictive hosting environment is not ideal, especially since the recommended setup makes use of Docker. + +#### ✅ Ensure the Host is reachable in the way you need it + +Typically, this means that your host is accessible on the open internet. + +However, you might have different constraints, such as only needing the host to be accessible from within a VPN or a local network. + +
+ +Once you have setup a host which is reachable in the way you need it to be, you can begin configuring Prose on it. + +## Configure & Deploy Prose + +### Without Docker + +Naturally, setting up Prose without Docker involves more manual steps and can be platform-specific. + +We have already covered this approach well in the tutorial. In particular, you can dig in with it on the page, [Setup the Prose GraphQL API](tutorial/install-prose#install-manually) + +### With Docker + +The Prose Docker image comes preconfigured to run Prose using **[PM2](https://pm2.keymetrics.io/)**, which is a sophisticated toolset for running Node processes in production. + +This is typically a reasonable setup, with which you can even expose the PM2 server directly to requests on the host. + +However, if you'd prefer a different setup, perhaps using Nginx as a reverse proxy to the Docker container, feel free to modify the Dockerfile to match your requirements. + +#### Install Docker + +**[Docker](https://www.docker.com/)** is a containerization framework that makes it easy to build, manage, and deploy your application stack in a way that is safer, more reliable, and reproducible across multiple platforms. + +There are countless guides available for installing Docker on a given operating system. + +Ubuntu is one of the more common operating systems avaiable through most cloud providers. + +Docker provides a [full tutorial](https://docs.docker.com/engine/install/ubuntu/) for this, and even provides a convenience script that you can run in two lines: + +```sh +curl -fsSL https://get.docker.com -o get-docker.sh +sudo sh get-docker.sh +``` + +Whichever path you need to take, just make sure that Docker is up and running on your host before continuing. + +#### Configure Environment Variables + +A comprehensive list of all Prose environment variables can be found on the [Environment Variables](env-prose) page. + +In brief, at a minimum, you'll want to ensure that `PROSE_DISCOURSE_HOST` is set. + +Another variable to pay attention to is `PROSE_APP_PORT`. This defaults to port 80, which instructs Prose to listen on that port. + +Depending on your setup, you might want it to listen on a different port. + +
+ +#### Build Prose from the Dockerfile + +If you'd like to use Docker to manually build Prose, run the following command from the **project root**. + +This might be of interest to you if you'd like to make some adjustments to the Dockerfile itself. + +Alternatively, if you simply wish to pull the latest Prose build from Docker Hub, you can [skip to the next step](#pulling-the-prose-docker-image). + +Unless you've made modifications to the Dockerfile and have it stored elsewhere, you can get started building by running: + +```bash +docker build -t prose:latest -f api/deploy/Dockerfile api/ +``` + +The command searches for the `Dockerfile` at `api/deploy/Dockerfile` because we instructed it to look there with the `-f` flag. + +Then, it uses `api/` as the context for the build, which allows the references in the `Dockerfile` to resolve correctly. + +By passing the `-t prose:latest` tag, it tags the locally built image as the latest build. This can be useful for identifying and managing the images in a Docker environment over time. + +#### Pull the Prose Docker Image + +If you'd rather just use the latest release of the Prose image, you can simply run: + +``` +docker pull kodefox/prose:latest +``` + +#### Run the Prose Docker Container + +Next, to run the newly built image, run the following command: + +```bash +docker run -d \ + -e PROSE_DISCOURSE_HOST=https://discourse.example.com \ + -e PROSE_APP_PORT=4000 \ + --name prose \ + -p 5000:4000 \ + kodefox/prose:latest +``` + +:::note +If you built the image by hand, you'll want to substitute `kodefox/prose:latest` with the image name and tag you used, such as `prose:latest`. +::: + +To recap, let's briefly break down that command line-by-line + +**Run in Detached Mode** + +```bash +docker run -d +``` + +The first line lets Docker know to run the container in **detached mode**. + +This means that the command will run in the background, will not be tied to your current session, and will keep running even if you log out. + +If you omitted the `-d` flag, Docker would run the container in the foreground, and exiting the process in the foreground would stop the container. + +**Set Environment Variables** + +```bash +-e PROSE_DISCOURSE_HOST=https://discourse.example.com +-e PROSE_APP_PORT=4000 +``` + +These lines instruct Docker to pass the environment variables of `PROSE_DISCOURSE_HOST` and `PROSE_APP_PORT` to the container when running it. + +These are both application-level environment variables that Prose itself will leverage to run properly. + +The Docker image expects these values to be set and passes them to the container's environment, which Prose then accesses via `process.env`. + +**Name the Container** + +```bash +--name prose +``` + +This line tells Docker to give the running container a name. This makes it easier to identify and interact with via commands, such as: + +```bash +docker stop prose +``` + +**Configure a Port Mapping between the Host and the Container** + +```bash +-p 5000:4000 +``` + +Next, we configure Docker with a port mapping, which tells Docker to listen to map the host port of `5000` to the container port of `4000`. + +Because we previously set `PROSE_APP_PORT=4000`, this means that all requests to the host at port `5000` will be forwarded to Prose inside of its container on port `4000`. + +```bash +kodefox/prose:latest +``` + +The last line of the command tells Docker which image to use for the container. + +Above, if you built the Prose image by hand, it was tagged as `prose:latest`. + +If you chose to pull from Docker Hub, this is simply instructing Docker to pull that image if necessary, and then start the container with it. + +#### Next Steps + +At this point, you should have a Docker container running the Prose server on your host. + +However, in terms of preparing your Prose host for production, you aren't quite there yet. + +Below, we'll guide you through the last steps, finalizing your deployment of the Prose GraphQL Server. + +#### Setup SSL (IMPORTANT) + +:::danger +Deploying Prose without SSL in a way that is publicly accessible is **extremely risky**. + +Doing so could provide an attacker with full access to your Discourse site and all of its data. +::: + +The **most important next-step** to take at this point is to configure an SSL certificate for your Prose host. + +The reason this is so important is that, without SSL, Prose's traffic between your users' devices and Discourse is not encrypted. + +And this means that attackers can snoop on your users' requests to Prose and Discourse—including, importantly, their authentication information. + +To put it bluntly, deploying Prose without configuring SSL is irresponsible and compromises the security of your Discourse instance. + +An attacker could even steal your authentication token and use it to access, and potentially destroy, your Discourse site. + +##### How to Setup SSL + +There are a variety of methods to obtain SSL certificates. Some are free, and some are paid. + +The free route involves using [Let's Encrypt](https://letsencrypt.org/), which is very useful, but can require more technical knowledge to setup correctly—depending on your configuration. A key difference is that you need to renew the certificates more frequently. + +The paid route involves using a provider like [DigiCert](https://www.digicert.com/) to obtain certificates that take longer to expire. + +Either way, you'll end up with certificate files that you can configure and launch your webserver with. + +Ideally, at this point, you've already purchased a domain. If you haven't, we'd recommend using a domain provider to get a low-cost domain name. + +You could host Prose at a subdomain of your existing Discourse site, like `prose.mydiscoursesite.com`. + +Or, you could just get a cheap, nonsense domain, like `purplemonkeydishwasher.tech`—since your users won't typically see it anyway. + +Regardless, to emphasize it again, it is **critical** that you don't deploy Prose into production until you have prepared your host to encrypt the traffic from Prose. + +#### Determine how you'll expose Prose on the host + +When someone navigates to your host which is running Prose, how will their request get routed to Prose? + +If you had exposed Prose directly on port 80—NOT recommended—and your host's domain name was `myproseserver.com`, then a user would navigate to `http://myproseserver.com` and be greeted with the [GraphiQL interface](https://www.graphql-yoga.com/docs/features/graphiql). + +However, a more common approach is to use a dedicated webserver, such as Nginx or Apache, that acts as a reverse-proxy. + +With this approach, the websever listens for all requests on the ports you tell it to, and is configured to route traffic to Prose, which is listening on a non-privileged port, like 8080. + +We recommend this approach more highly for the following reasons: + +- Existing webservers are generally more reliable and performant +- It allows configuration of an SSL certificate, which is necessary for protecting your users' data + +Upon configuring the webserver, you'll need to instruct it to forward traffic to the running Prose server. + +Your setup might look something like this: + +- Nginx is configured to listen on port 80 and port 443 on your domain, `purplemonkeydishwasher.tech` +- Nginx has located and loaded your SSL certificate files for `purplemonkeydishwasher.tech` +- Nginx is configured to upgrade all requests on port 80 to port 443 +- Your Prose server is running inside of Docker on a container port of 80, and exposed to the host on port 8080. +- Your Nginx configuration specifies that requests to `purplemonkeydishwasher.tech` should be forwarded to port 8080. +- Requests come in for `purplemonkeydishwasher.tech`, Nginx routes it to the container running Prose, which handles the requests, and responds. + +#### Configure your Cloud Provider's Firewall, if one exists + +Ideally, you've configured Prose to be exposed on the open internet with the traffic encrypted over port 443. + +Depending on your cloud provider, you may need to go into its settings and expose that port on the firewall. + +For example, in DigitalOcean, this involves going to the Networking section, and creating a new firewall rule. + +From there, it is fairly simple to add common ports, like 80 and 443, to the firewall. + +After that, you simply apply the firewall to your particular instance, and traffic should be allowed through. + +#### Configure DNS Settings for your Domain + +Provided that you've already registered a domain name, you'll need to configure it so that the domain name points to your host which is running Prose. + +Depending on your setup, this will either be done in your domain provider's settings panel, or perhaps within your cloud provider. + +Continuing with the DigitalOcean example from above, you can configure your domain provider to point at DigitalOcean's name servers. + +This effectively tells your domain provider that DigitalOcean will handle everything for you, and allows you to make adjustments to your domain from within DigitalOcean. + +In that case, DigitalOcean makes it seamless to map the domain name to your instance's IP address, and it should then be accessible from the domain name. + +Otherwise, you'll want to get the IP address of your host, go into your domain provider, and instruct it that requests to your domain should be direct to your host's IP address. + +#### Ready to Go + +At this point, your deployed host should be running Prose correctly. When you navigate to the domain name that you configured it with, you should see [GraphiQL](https://www.graphql-yoga.com/docs/features/graphiql), which will allow you to make GraphQL queries against your Discourse instance. + +We understand that the details of your deployment can vary quite a bit depending on how you chose to do it. + +If you run into any issues with this step—as always—don't hesitate to reach out to us for support. diff --git a/documentation/versioned_docs/version-2.2.0/deployment.md b/documentation/versioned_docs/version-2.2.0/deployment.md new file mode 100644 index 00000000..b46ab73b --- /dev/null +++ b/documentation/versioned_docs/version-2.2.0/deployment.md @@ -0,0 +1,86 @@ +--- +title: Overview +--- + +As covered in [Concepts and Architecture](concepts#prose-discourse-through-graphql), Prose is Lexicon's GraphQL API layer on top of the traditional RESTful API provided by Discourse. + +## Getting Started with Deployment + +At this point, you're likely to be digging into this section of the documentation for two reasons: + +- You've been developing against a local instance (or container) running Prose, and you're ready to actually deploy your entire Lexicon project to production. + +- You want to simplify your development process by pointing the Lexicon Mobile App at a deployed instance of Prose. + +In either scenario, the end goal of this section is to have a working Prose server accessible on the open internet. + +### 🔐 Note about Access Control + +As a brief aside, please note that Prose cannot expose any information from Discourse that Discourse is not already exposing on its own. + +If your Discourse instance requires authentication, then Prose will be unable to retrieve most queries unless the required authentication information is provided by the user accessing Prose. + +### 🧱 Alternative Deployment Strategies + +Initially, we wanted to provide instructions for an integrated deployment strategy. This would have involved deploying Prose on the same host as your Discourse instance, and ideally finding a way to deploy and expose it within the running Docker host that Discourse uses itself. + +This is still achieveable. But for now, we have opted to focus solely on deploying Prose as a dedicated instance. + +However, should you find yourself preferring a custom deployment of Prose, we would encourage you to do so. + +If you do, and you have some questions or challenges you're encountering, please reach out to us. + +Ideally we can help you sort things out and work your approach into our documentation so that everyone will benefit going forward. + +## Deploying as Dedicated Instance + +As mentioned above, the official deployment strategy for Prose is to host it as a dedicated instance. + +Like anything, this comes with both benefits and trade-offs, which we have outlined for you below. + +### 🚀 Benefits + +A dedicated host for Prose will have better performance and reliability because its only resource usage comes from running Prose. i.e., it has exclusive usage of CPU, RAM, disk space, etc. + +If, on the other hand, you had managed to deploy Prose on the same host as your Discourse instance, this would mean that both Prose and Discourse need to share the host's allocated resources. If your Discourse instance is already running on a fairly light host, running Prose on it might mean that you would need to upgrade to a host with more resources. + +### ⚠️ Possible Trade-offs + +#### Increased Cost + +Naturally, if you're setting up a dedicated host to run Prose, then that involves additional costs on top of what you're already paying to host Discourse. + +Having said that, for most deployments, it is unlikely that you will need to allocate an expensive amount of resources to Prose. + +For example, on Digital Ocean, the $5 Shared CPU node is often sufficient. + +#### Potential for Increased Latency + +By nature, when deploying Prose on a different host from your running Discourse instance, the latency between the mobile app and Discourse increases. + +This is because each request has to make two hops: + +- The first request is from the client (your Lexicon-powered mobile app) to the Prose GraphQL API +- The second request is from Prose to Discourse + +However, the only important questions regarding this point are: + +- How much measurable latency is there? +- Is it noticeably slow to myself or my users? + +This, of course, can depend on several factors: + +- Where your Discourse server is deployed +- Where your Prose server is deployed +- Where your users tend to be +- If the amount of traffic (load) is too much for the system to optimally run both Prose and Discourse. + +If you are observing noticeable latency, we would recommend looking into these factors. + +Ideally, you'll want to deploy Prose in the same region as your Discourse instance; and it is even better if you can deploy Prose in the same datacenter as your Discourse instance. + +## Up Next + +With this overview out of the way, we'll start by introducing you to the list of all possible [environment variables](env-prose) that may be necessary or useful when deploying Prose. + +Lastly, we'll get into the heart of it, by [preparing your host and deploying Prose](dedicated). diff --git a/documentation/versioned_docs/version-2.2.0/discourse-features.md b/documentation/versioned_docs/version-2.2.0/discourse-features.md new file mode 100644 index 00000000..fa303920 --- /dev/null +++ b/documentation/versioned_docs/version-2.2.0/discourse-features.md @@ -0,0 +1,57 @@ +--- +title: Discourse Features Support +--- + +Below is a table of Discourse features which provides the details and current status about the support for a given feature in the **Lexicon Mobile App**. + +If we missed one, or anything looks out of date here, don't hesitate to submit a Pull Request which updates the table. + +Is the feature you love not supported? [Reach out to us](mailto:support@kodefox.com) to discuss how we can bring it to life for you. + +#### Our General Approach to Feature Support + +Much of our initial focus was on using-facing features, rather than administrative features. + +This is why, for example, users can select categories for their topics, but administrators are unable to create new categories from within the mobile app. + +For this reason, most admin tasks are still best accomplished using the Discourse web app on a larger device. + +### Lexicon Mobile App Features + +| Feature | Description | Supported | Notes | +| ------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | --------- | --------------------------------------------------------------------------------------------------------------------- | +| 2FA Login | Allow users with 2FA enabled to be prompted for their 2FA code when logging in | ✅ | Managing 2FA, such as enabling it or disabling it from within the app, is not currently supported | +| Ability to Tag Topics | Create and tag topics to provide relevant metadata for your users | ✅ 🔧 | Configuration required: see [Optimal Experience](optimal#enable-topic-tagging) | +| Topic Previews (Excerpts) | Show an excerpt of the first post in a topic from the Home screen | ✅ 🔧 | Configuration required: see [Optimal Experience](optimal#enable-topic-excerpts) | +| View User Activity | View a user's recent activity—such as topics, posts, and likes—in a single feed from their profile | ✅ | The ability to filter by activity is not currently supported | +| Topic Metrics | Likes, Views, Replies, and Frequent Posters | ✅ | | +| Topic & Post Actions | Ability to like and edit topics and posts | ✅ | | +| View Top & Latest Topics | A Tab View at the top of the main feed provides the ability to switch between Latest and Top activity | ✅ | | +| Search | Search the current Discourse instance for topics and posts based on keywords, categories, and tags | ✅ | | +| Categories | View the category of a topic and filter topics by a given category | ✅ | Categories cannot be created, updated, or deleted | +| Attaching Media to Posts | Users can attach media to a post from the app | ✅ 🔧 | Configuration recommended for supported file extensions-see [Optimal Experience](optimal#configure-upload-extensions) | +| Standard Markdown | Standard Markdown is supported in the editor and rendered correctly in the mobile app | ✅ | Light, incomplete support exists for some of Discourse's custom markup, such as dates | +| Sign Up | Allow users to sign up for an account directly through the mobile app, depending on whether your Discourse instance allows new user registration or not | ✅ | | +| Browsing Public Instances | Allow users to immediately access and browse your Discourse instance from the mobile app if it is not private | ✅ | Users will be prompted to login upon attempting an authenticated action | +| User Profiles | Ability to view users' profiles and edit your own | ✅ | Partial support: displays the user's photo, username, Markdown bio on a single line, and recent activity | +| Post Flagging | Allow users to flag posts for admins to review | ✅ | Admins are not able to review posts in the app, though they will see in-app notifications for flags | +| Mark Discourse Notifications Read | Allow users to see new notifications from the profile screen of the mobile app and mark all notifications as read | ✅ | Some notifications from Discourse are not tappable in the mobile app, such as badge notifications | +| Private messaging | Allow users to start private or group messages with one another | ✅ | | +| Mentions | Allow users to mention a user when creating or editing posts and messages | ✅ | +| Color Scheme | Provides light and dark mode support for users | ✅ | Specify color scheme (light mode, dark mode, or system) from within the app (only local to the user's mobile device) | +| Discourse Emojis | Utilize emojis when creating a topic, making a post, or sending a reply | ✅ | Discourse BB Code emojis and Unicode-based emojis are fully supported. | +| User Status | Allow users to update their statuses and view the statuses of other users | ✅ | | +| Polls | Allow users to create polls with custom settings in posts and private messages. Enable users to view and vote on the polls. | ✅ | | +| Button Bar for Markup Text | Allows users to automatically create Markdown formatting for posts and messages | ✅ | Supports automatic creation of formatting for bold, italic, quoted text, bullet lists, and numbered lists | +| Sign in With Apple | Allows users to log in using their Apple account | ✅ | Apple email account needs to be registered first on Discourse | +| Login With Link | Allows users to log in using an email login link without inserting a password | ✅ | | +| Activation Account With Link | Enables users to log in to the Lexicon-powered mobile app after activating their account upon signing up. Users receive an activation email from Discourse. | ✅ | | +| Badges | The ability to see and interact with badges that have been awarded to users on the Discourse instance | ❌ | | +| Post Drafts | Enable users to start composing a draft of a post and return to it later | ❌ | | +| Groups | Enable users to create and participate in private groups of which only group members can view certain topics | ❌ | | +| Admin Features | Discourse admin features generally not available in Lexicon—better suited to a desktop environment | ❌ | Editing posts is supported | +| Post Quotes, Toggles, and Task Lists | Custom text formatting that enables Discourse-specific features | ❌ | | +| Post Bookmarks | Allow users to bookmark certain posts or topics | ❌ | | +| DiscourseConnect (SSO) | Replace Discourse authentication with a Custom Provider | ❌ | | +| Custom Authentication Plugins | Login via OAuth2 or other protocols using custom Discourse Plugins | ❌ | | +| Real-time Chat | Enable users to initiate conversations using the chat feature, either in a channel or through private messaging | ❌ | | diff --git a/documentation/versioned_docs/version-2.2.0/discourse-plugin-enable.md b/documentation/versioned_docs/version-2.2.0/discourse-plugin-enable.md new file mode 100644 index 00000000..4308ebc0 --- /dev/null +++ b/documentation/versioned_docs/version-2.2.0/discourse-plugin-enable.md @@ -0,0 +1,54 @@ +--- +title: Enable the Lexicon Plugin +slug: discourse-plugin/enable +--- + +import useBaseUrl from '@docusaurus/useBaseUrl'; + +--- + +After you have confirmed the plugin has been installed and your Discourse instance is running again, you can follow these steps to enable the plugin: + +1. As an admin user, access your Discourse admin dashboard. + +2. Navigate to the `Plugins` tab. + +You'll notice that the `discourse-lexicon-plugin` is not enabled yet. + +Plugin Admin Page + +3. Click on the `Settings` button for the `discourse-lexicon-plugin` entry. + +4. Select the feature you want to enable and turn it on. + +### Push Notifications + +For push notifications, all you need to do is check the box for `lexicon push notifications enabled`. This is covered in [Enable Push Notifications](./push-notifications/setup/enable-push-notifications.md). + +### Email Deep Linking + +For email deep linking, you need to fill in your app scheme first before enabling it. + +Plugin Settings Page + +This is covered in detail in [Enable Email Deep Linking](./email-deep-linking/setup/enable-email-deep-linking.md). + +### Login With Link + +For Login with Link, you need to fill in your app scheme first before enabling it and check the box for `Lexicon Login Link Enabled`. + +This is covered in detail in [Enable Login With Link](./login-with-link/setup/enable-login-with-link.md). + +### Activation Account With Link + +For activation account with link, you need to fill in your app scheme first before enabling it. + +Plugin Settings Page + +This is covered in detail in [Enable Activation Account With link](./activation-with-link/setup/enable-activate-with-link.md). + +##### Login With Apple + +For Login with Apple, you need to fill in your app bundle ID first before enabling it and check the box for `Lexicon Apple Login Enabled`. + +This is covered in detail in [Enable Login With Apple](./login-with-apple/setup/enable-login-with-apple.md). diff --git a/documentation/versioned_docs/version-2.2.0/discourse-plugin-installation.md b/documentation/versioned_docs/version-2.2.0/discourse-plugin-installation.md new file mode 100644 index 00000000..911770b1 --- /dev/null +++ b/documentation/versioned_docs/version-2.2.0/discourse-plugin-installation.md @@ -0,0 +1,82 @@ +--- +title: Plugin Installation +slug: discourse-plugin/setup +--- + +Before you can start using the Lexicon Discourse Plugin, there are a few prerequisites and installation steps you need to follow. This documentation will guide you through the process, ensuring a smooth setup of the plugin on your site. + +## Prerequisites + +In order to use this plugin, you must have access to your Discourse server in a way which allows you to modify the server's `app.yml`. If a hosting provider is managing Discourse for you, you will have to contact them to request that they install the plugin on your behalf. + +Specifically, you will need the ability to install plugins, which means directly modifying `/var/discourse/containers/app.yml` to include the [Lexicon Discourse plugin](https://github.com/lexiconhq/discourse-lexicon-plugin.git), and then rebuilding your site. + +## Plugin Installation Steps + +### Access your Server + +Login to your underlying Discourse host server via SSH. + +This is specific to each hosting setup, but typically you will need to use a terminal application such as Terminal on macOS or PuTTY on Windows. + +### Open the Discourse `app.yml` file + +Feel free to use your terminal editor of choice (vim, emacs, nano, etc.). + +:::note +You may need `sudo` access to edit the file, but it depends on how the server was configured. +::: + +```bash +vim /var/discourse/containers/app.yml +``` + +### Get the Plugin’s Git Clone URL + +Discourse plugins are referenced by their reachable Git clone URLs, which typically end with `.git`. + +The Git clone URL for the [Lexicon Discourse plugin](https://github.com/lexiconhq/discourse-lexicon-plugin) can be found below: + +``` +https://github.com/lexiconhq/discourse-lexicon-plugin.git +``` + +Copy it to your clipboard for use in the next step. + +### Add the plugin’s repository URL to your container’s `app.yml` file: + +Add the plugin’s Git clone url to the section below. + +``` +hooks: + after_code: + - exec: + cd: $home/plugins + cmd: + - git clone https://github.com/lexiconhq/discourse-lexicon-plugin.git +``` + +### Rebuild the container, with caution + +:::caution +Please be aware that rebuilding your site will result in your site going offline for a period of time, typically between 5 to 30 minutes. We advise proceeding carefully and taking precautions outlined below. +::: + +#### Precautionary Measures + +1. Before installing the plugin or performing any site rebuild, it is highly recommended to create a backup of your Discourse site. +1. It is advisable to upgrade your Discourse installation and all existing plugins to their latest versions before attempting to install this plugin. +1. Although rare, there may be situations where the site does not come back online after the rebuilding process, and requires further troubleshooting to revive. + - This is always a risk when installing a plugin or performing any task that requires rebuilding the app. + - We recommend that you perform these changes at a time that minimizes the users affected and that you have a well-defined contingency plan in place if something goes wrong. + +#### Run Rebuild Command + +```bash +cd /var/discourse +./launcher rebuild app +``` + +### How to Uninstall the Plugin + +To remove the plugin, simply remove the Git clone URL line from your `app.yml` file and rebuild your site. Please keep in mind that rebuilding your site will will result in your site going offline for a period of time, and poses the same risks that come with rebuilding the app. diff --git a/documentation/versioned_docs/version-2.2.0/discourse-plugin.md b/documentation/versioned_docs/version-2.2.0/discourse-plugin.md new file mode 100644 index 00000000..99e31b90 --- /dev/null +++ b/documentation/versioned_docs/version-2.2.0/discourse-plugin.md @@ -0,0 +1,23 @@ +--- +title: Introduction +slug: discourse-plugin/ +--- + +import useBaseUrl from '@docusaurus/useBaseUrl'; + +--- + +As of Lexicon version 2.0.0, a custom Discourse plugin is available to provide a more seamless mobile integration between Discourse and your Lexicon-powered mobile app. + +The plugin offers two key features for version 2.0.0: + +- **Push notifications**: support for native push notifications on user's mobile devices, according to relevant activity on your Discourse site. Powered by Expo's [push notifications service](https://docs.expo.dev/push-notifications/overview/). +- **Email deep linking**: custom deep links in emails from Discourse, allowing users to seamlessly launch your Lexicon-powered mobile app directly from their mobile email client. + +As of Lexicon version 2.2.0, we have added more features to the Discourse Lexicon plugin: + +The plugin now offers three additional features: + +- **Sign in with Apple**: Support for Lexicon-powered mobile apps to enable login using an Apple account. +- **Login with Link**: Support for Lexicon-powered mobile apps to enable login using a link from an email without needing to input a password in the app. [Login with Link documentation](login-with-link/intro.md) +- **Activation with Link**: Support for activating an account after sign-up for Lexicon-powered mobile apps. diff --git a/documentation/versioned_docs/version-2.2.0/email-deep-linking/intro.md b/documentation/versioned_docs/version-2.2.0/email-deep-linking/intro.md new file mode 100644 index 00000000..ca863368 --- /dev/null +++ b/documentation/versioned_docs/version-2.2.0/email-deep-linking/intro.md @@ -0,0 +1,9 @@ +--- +id: intro +title: Introduction +slug: discourse-plugin/email-deep-linking +--- + +The Lexicon Discourse plugin provides support for integrating Discourse's email notifications with your Lexicon-powered mobile app. Our plugin modifies links in specific Discourse emails so that when a relevant link is tapped and the user has your Lexicon-powered mobile app installed, it will open the app to the relevant topic or post. Otherwise, it will fall back to opening the topic in the device's web browser as it normally would. + +This section of the documentation offers step-by-step instructions to integrate email deep linking into your Discourse site so that your users have a more seamless experience with your Lexicon-powered mobile app. diff --git a/documentation/versioned_docs/version-2.2.0/email-deep-linking/setup/enable-email-deep-linking.md b/documentation/versioned_docs/version-2.2.0/email-deep-linking/setup/enable-email-deep-linking.md new file mode 100644 index 00000000..5e794ddd --- /dev/null +++ b/documentation/versioned_docs/version-2.2.0/email-deep-linking/setup/enable-email-deep-linking.md @@ -0,0 +1,25 @@ +--- +title: Enabling the Email Deep Linking +--- + +import useBaseUrl from '@docusaurus/useBaseUrl'; + +This guide will walk you through the necessary steps to activate email deep linking on your Discourse site. + +## Steps + +1. Access your Discourse admin dashboard. + +2. Navigate to the `Plugins` section. + + + +3. Locate the `discourse-lexicon-plugin` and click on the `Settings` button. + +4. Fill in the `lexicon app scheme` setting with your app scheme. The app scheme is required to enable email deep linking. + +5. Check the `lexicon email deep linking enabled` box in the Lexicon settings section and save your changes. + + + +Once the email deep linking feature is enabled, you will be able to utilize its functionality in your Discourse instance. diff --git a/documentation/versioned_docs/version-2.2.0/email-deep-linking/setup/verify-email-deep-linking.md b/documentation/versioned_docs/version-2.2.0/email-deep-linking/setup/verify-email-deep-linking.md new file mode 100644 index 00000000..0d3187d0 --- /dev/null +++ b/documentation/versioned_docs/version-2.2.0/email-deep-linking/setup/verify-email-deep-linking.md @@ -0,0 +1,49 @@ +--- +title: Verify Email Deep Linking +slug: verify-email-deep-linking +--- + +import useBaseUrl from '@docusaurus/useBaseUrl'; + +:::note +The steps below assume that **you have already published your Lexicon-powered mobile app** to the App Store and/or Google Play Store **with the correct app scheme**. If you are running the app on your machine locally through Expo, you should not expect the steps to work. +::: + +This guide will provide you with step-by-step instructions to help you validate the functionality of email deep linking within your Lexicon mobile app. + +## Pre-requisites + +:::note +If you have not yet fulfilled all of the pre-requisites below, this test will not work as expected. +::: + +In order to test email deep linking properly: + +1. You **must** have already published your Lexicon-powered mobile app to the App Store and/or Google Play Store. +1. You have already installed and configured the Lexicon Discourse plugin on your Discourse site. +1. You have enabled email deep linking within the Lexicon Discourse plugin settings, and the app scheme matches what you published your app with. +1. You have at least 1 mobile device with your Lexicon-powered mobile app already installed, with the correct app scheme as it was configured in Discourse. +1. You have at least 2 separate Discourse accounts to test with. +1. Ensure your Discourse site allows **mailing list mode**, and that it is turned on for the accounts you are testing with. + - If you do not do this, you will have to wait for Discourse to send its next digest email, which could take a while. + +## Steps + +To test email deep linking within your **published** Lexicon-powered mobile app, follow these steps: + +1. Ensure you have access to at least 2 separate accounts on your Discourse instance. +1. On your mobile device, open your Lexicon-powered mobile app and login using one of your accounts. + - **Note**: ensure that your email client on your mobile device will receive emails for this account. +1. Open your Discourse site in a web browser on your laptop or desktop computer. +1. Login to your **second** Discourse account in your web browser. +1. On your mobile device, using the **first** account, create a new post. +1. Now, on your laptop or desktop computer, using the **second** account, find the post you created on the mobile app and reply to it. +1. Back on your mobile device, you should receive an email notification from Discourse about the reply from the second account. +1. Click on the button that says `Visit Message` or `Visit Topic`. The label depends on what activity generated the email (see screenshot below). +1. The link will first open in your mobile web browser. Provided that the Lexicon-powered mobile app is installed and matches the configured app scheme, it should automatically open your app to the relevant topic or message scene. + +
+ +
+ +And that's it! You have successfully completed the steps to enable and test email deep linking in your app. diff --git a/documentation/versioned_docs/version-2.2.0/env-mobile.md b/documentation/versioned_docs/version-2.2.0/env-mobile.md new file mode 100644 index 00000000..79af9933 --- /dev/null +++ b/documentation/versioned_docs/version-2.2.0/env-mobile.md @@ -0,0 +1,119 @@ +--- +title: Configuration Values +--- + +You can check and set the configuration values in `frontend/Config.ts`. + +The table below describes the configuration values for the Lexicon Mobile App. + +If there is a default value indicated, you do not need to set it. + +| Variable | Required | Notes | Default Value | Example Value(s) | +| -------------------- | -------- | -------------------------------------------------------------------------------------- | ------------- | ------------------------------------------------------------------------------------------------------------------------------------- | +| proseUrl | Yes | The url of the Prose Server (must start with http or https) | - | https://prose.myserver.com https://prose.myserver.com:8080 https://prose.myserver.com/subpath https://prose.myserver.com:8080/subpath | +| inferDevelopmentHost | No | The flag (true / false) to override localhost with the host of the development machine | (empty) | true | + +## The `config` object + +In the `Config.ts` file, you'll find a `config` object that allows you to specify configuration values by scenario. + +The two primary scenarios are: + +- `localDevelopment`: when developing against the app locally. This configuration is also used as a fallback for an unknown build channel. +- `buildChannels`: used to define configuration by build channel when building the app with the EAS CLI. + +Primarily, you'll only be concerned with configuring `proseUrl` for each of these sections. + +## `proseUrl` + +:::caution +`proseUrl` must always be specified, with or without a port number, and must always start with either `http://` or `https://`. +::: + +`proseUrl` is used to specify the URL of the Prose GraphQL API. + +The Prose GraphQL API acts a middleman between the Lexicon Mobile App and your Discourse instance. Without it, the mobile app cannot interact with your Discourse instance. + +### Example + +```ts +const config = { + localDevelopment: { + proseUrl: 'http://localhost:8929', + }, + buildChannels: { + preview: { + proseUrl: 'https://preview.myserver.com', + }, + production: { + proseUrl: 'https://prose.myserver.com', + }, + }, +}; +``` + +With this configuration above, the app will: + +- point at `http://localhost:8929` when you run the app using `npm run start` +- point at `https://preview.myserver.com` when you build the app using `eas build --profile preview` +- point at `https://prose.myserver.com` when you build the app using `eas build` + +`proseUrl` also can include a subpath if desired: + +```ts +const config = { + localDevelopment: { + proseUrl: 'http://localhost:8929', + }, + buildChannels: { + preview: { + proseUrl: 'https://preview.myserver.com:8080/subpath', + }, + production: { + proseUrl: 'https://myserver.com/api/prose', + }, + }, +}; +``` + +**Different Behavior in Development** + +When running the app locally, if `proseUrl` is set to `http://localhost` or `http://127.0.0.1`, it will replace `proseUrl` with the IP address of your development machine. It does this by using Expo's `debuggerHost` constant. + +_Note: this does not apply when building the app._ + +This addresses multiple issues: + +- Accessing `localhost` from within the Android simulator does not map to your development machine +- Accessing `localhost` from a device running Expo Go does not map to your development machine + +Both of these scenarios would otherwise require you to manually identify and specify your development machine's IP address with `proseUrl`. This is bothersome since your machine's IP address can change over time. + +If you are interested in more details about this, the implementation of this behavior is available in `frontend/constants/app.ts`. + +This behavior of automatically overriding those values can be disabled, with `inferDevelopmentHost`, which is covered below. + +## `inferDevelopmentHost` + +:::info +This flag is only valid under `localDevelopment`. It has no effect when used as part of `buildChannels`. +::: + +When in development, by default, the Lexicon Mobile App will check to see if `proseUrl` is set to either `http://localhost` or `http://127.0.0.1`. + +When detected, either of those values will be overwritten with the IP address of your development machine. + +This is a very useful feature that makes on-device testing simply work out of the box without needing to manually specify your IP address (or update it when it changes). + +For scenarios where this behavior is not desirable, `inferDevelopmentHost` can be used as a flag to disable this behavior. It can be disabled by specifying the value as `false`. + +When set to `false`, this behavior of overriding `proseUrl` with the development machine's IP address will no longer occur, and the original value will be passed through as-is. + +```ts +const config = { + localDevelopment: { + proseUrl: 'http://localhost:8929', + inferDevelopmentHost: false, + }, +}; +``` diff --git a/documentation/versioned_docs/version-2.2.0/env-prose.md b/documentation/versioned_docs/version-2.2.0/env-prose.md new file mode 100644 index 00000000..59901fd5 --- /dev/null +++ b/documentation/versioned_docs/version-2.2.0/env-prose.md @@ -0,0 +1,15 @@ +--- +title: Prose Environment Variables +--- + +The table below lays out environment variables for the Prose GraphQL API. + +If there is a default value indicated, you do not need to set it. + +| Environment Variable | Required | Notes | Default Value | Example Value | +| --------------------------- | -------- | ----------------------------------------------------------------------------------- | ---------------------- | ------------------------------------ | +| PROSE_DISCOURSE_HOST | Yes | The specific location of your Discourse instance. | - | https://discourse.example.com | +| PROSE_DISCOURSE_UPLOAD_HOST | No | Instruct Prose to use a different host for file uploads to Discourse. | | https://upload.discourse.example.com | +| PROSE_APP_HOSTNAME | No | The **application-level** hostname that Prose will listen on. | localhost | 0.0.0.0 | +| PROSE_APP_PORT | No | The **application-level** port that Prose will listen on. | 80 | 8080 | +| SKIP_CHECK_DISCOURSE | No | Bypass the startup process of checking the provided Discourse host for reachability | false | true | diff --git a/documentation/versioned_docs/version-2.2.0/intro.md b/documentation/versioned_docs/version-2.2.0/intro.md new file mode 100644 index 00000000..f358899d --- /dev/null +++ b/documentation/versioned_docs/version-2.2.0/intro.md @@ -0,0 +1,142 @@ +--- +id: intro +title: Introduction +slug: / +--- + + + --- iOS Auth + + + + --- iOS Dark Mode + + + + --- iOS Comment + + + + --- iOS Message + + --- Android Auth + + + + --- Android Dark Mode + + + + --- Android Comment + + + + --- Android Message + + + +import useBaseUrl from '@docusaurus/useBaseUrl'; +import Carousel from 'react-bootstrap/Carousel'; + +--- + +Lexicon is a customizable, pre-built mobile app that provides an elegant mobile discussions experience. Built on top of [Discourse](#what-is-discourse). + +## Features + +- Topics, Private Messaging, User Signups, Profile Management, and more +- Rapidly build Android and iPhone apps for your existing Discourse site +- [Push Notifications](./push-notifications/introduction.md) direct to your users' mobile devices +- More seamless native Discourse experience [with Email Deep Linking](./email-deep-linking/intro.md) +- Straightforward process to [**customize**](white-labeling) the app for your brand +- Backed by a [GraphQL](https://graphql.org/) API +- Free and open source! +- [Commercial support](commercial-support) available + +## Benefits + +- Launch a custom mobile discussions app +- Increase engagement with your users by adding a mobile-first Discourse experience—no more [WebViews](https://www.kirupa.com/apps/webview.htm). +- Built with [React Native](https://reactnative.dev/) and [Expo](https://expo.io), delivering a native look-and-feel on both iOS and Android. +- Includes an auto-documented [GraphQL](https://graphql.org/) [interface](concepts#prose-discourse-through-graphql) over the Discourse API, which you can build on top of. + +## Screenshots + +### iOS + + + + IOS Lexicon Login Page + IOS Lexicon Signup Page + IOS Lexicon Home Page + + + IOS Lexicon Dark Mode in Home Page + IOS Lexicon New Post Page + IOS Lexicon Post Detail Page + + + IOS Lexicon Comment Section + IOS Lexicon Profile Page + IOS Lexicon Notification Page + + + IOS Lexicon Message Page + + + +### Android + + + + Android Lexicon Login Page + Android Lexicon Signup Page + Android Lexicon Home Page + + + Android Lexicon Dark Mode in Home Page + Android Lexicon New Post Page + Android Lexicon Post Detail Page + + + Android Lexicon Comment Section + Android Lexicon Profile Page + Android Lexicon Notification Page + + + Android Lexicon Message Page + + + +## How does Lexicon work? + +Lexicon delivers a native mobile Discourse experience with **two key components**: + +- The [**Lexicon Mobile App**](#the-lexicon-mobile-app) - a modern mobile app built with [Expo](https://expo.io) & [React Native](https://reactnative.dev/) +- [**Prose**](#prose-discourse-through-graphql), our GraphQL API on top of the Discourse API + +### The Lexicon Mobile App + +The Lexicon Mobile App is built with [Expo](https://expo.io), which allows us to maintain both the iOS and Android apps with a single codebase. + +For those unfamiliar, Expo provides a superior development and deployment experience on top of [React Native](https://reactnative.dev/). + +### Prose: Discourse through GraphQL + +Prose is Lexicon's [GraphQL](https://graphql.org/) layer built on top of Discourse's API. + +This enables developers to quickly build apps on top of a live Discourse instance while leveraging the [benefits of GraphQL](https://www.apollographql.com/docs/intro/benefits/). + +### What is Discourse? + +Discourse is open-source **discussion software** that is thoughtfully designed, simple to setup, and well-maintained. + +You can learn more about it on the [Discourse website](https://www.discourse.org/). + +### Further Details + +You can learn about the technical details of our approach in [Concepts & Architecture](concepts). + +## License + +MIT. Copyright (c) [Lexicon](https://github.com/lexiconhq) diff --git a/documentation/versioned_docs/version-2.2.0/lexicon-updates.md b/documentation/versioned_docs/version-2.2.0/lexicon-updates.md new file mode 100644 index 00000000..43d95945 --- /dev/null +++ b/documentation/versioned_docs/version-2.2.0/lexicon-updates.md @@ -0,0 +1,17 @@ +--- +title: Receiving Updates from Lexicon +--- + +Due to the nature of this project, the best way to synchronize bugfixes, updates, and other changes to the Lexicon Mobile App is to treat your app like a fork of our repository. + +In the process of customizing the Lexicon Mobile App for your needs, you might make any number of changes to the theme or assets. + +However, the underlying codebase should be—for the most part—untouched. + +When we release a bugfix or new feature on the `master` branch, you'll be able to pull down our changes, resolve any conflicts with your changes, and have an updated version of your app ready to republish. + +It is worth acknowledging that this approach, which effectively uses Git to solve this problem in a fairly simple way, could be improved. + +Provided that there's enough interest, we might later decide to shape Lexicon into more of a standalone SDK package that you can import and receive updates to via npm. + +If you're interested in making that a reality, please reach out to us! diff --git a/documentation/versioned_docs/version-2.2.0/login-with-apple/intro.md b/documentation/versioned_docs/version-2.2.0/login-with-apple/intro.md new file mode 100644 index 00000000..85e5380a --- /dev/null +++ b/documentation/versioned_docs/version-2.2.0/login-with-apple/intro.md @@ -0,0 +1,9 @@ +--- +id: intro +title: Introduction +slug: discourse-plugin/login-with-apple +--- + +The Lexicon Discourse plugin provides support for integrating Apple's authentication with your Lexicon-powered mobile app. Our plugin enables signing into your Discourse site using Apple authentication. + +This section of the documentation offers step-by-step instructions to integrate this login functionality into your Discourse site, providing your users with a more seamless experience with your Lexicon-powered mobile app. diff --git a/documentation/versioned_docs/version-2.2.0/login-with-apple/setup/enable-login-with-apple.md b/documentation/versioned_docs/version-2.2.0/login-with-apple/setup/enable-login-with-apple.md new file mode 100644 index 00000000..18860cbc --- /dev/null +++ b/documentation/versioned_docs/version-2.2.0/login-with-apple/setup/enable-login-with-apple.md @@ -0,0 +1,29 @@ +--- +title: Enabling login with Apple +--- + +import useBaseUrl from '@docusaurus/useBaseUrl'; + +This guide will walk you through the necessary steps to activate login with Apple at lexicon app on your Discourse site. + +## Steps + +1. Access your Discourse admin dashboard. + +2. Navigate to the `Plugins` section. + + + +3. Locate the `discourse-lexicon-plugin` and click on the `Settings` button. + +4. Fill in the `lexicon apple client id` setting with your app bundle ID. The app bundle ID is required to enable login with Apple. If you haven't register an app bundle ID, you can follow the instructions in this [tutorial](../../app-store#register-a-new-bundle-id) to do so. + +
+ +
+ +5. Check the `lexicon apple login enabled` box in the Lexicon settings section and save your changes. + + + +Once the login with Apple feature is enabled, you will be able to utilize its functionality in your Discourse instance. diff --git a/documentation/versioned_docs/version-2.2.0/login-with-apple/setup/verify-login-with-apple.md b/documentation/versioned_docs/version-2.2.0/login-with-apple/setup/verify-login-with-apple.md new file mode 100644 index 00000000..a3212d72 --- /dev/null +++ b/documentation/versioned_docs/version-2.2.0/login-with-apple/setup/verify-login-with-apple.md @@ -0,0 +1,30 @@ +--- +title: Verify Login With Apple +--- + +import useBaseUrl from '@docusaurus/useBaseUrl'; + +Below, we'll walk you through how you can validate the functionality of logging in with Apple within your Lexicon-powered mobile app. + +:::info +In order to be able test logging in with Apple, **you will need to use Lexicon version 2.2.0** for your Lexicon app. +::: + +:::note +Ensure that the Bundle Identifier under iOS section in your `app.json` matches the one in your Discourse's plugin settings. +::: + +## Steps + +To test logging in with Apple within your Lexicon-powered mobile app, follow these steps: + +1. Ensure that you have Lexicon-powered mobile app at your iOS device. +2. On your mobile device, open your Lexicon-powered mobile app. +3. On the login screen, you will see a "Sign in with Apple" button. Click the button and confirm your Apple account. + - **Note**: Ensure that you have a registered account on Discourse using the same email as your Apple account. + +
+ +
+ +And that's it! You will be automatically logged in once your Apple account is confirmed. diff --git a/documentation/versioned_docs/version-2.2.0/login-with-link/intro.md b/documentation/versioned_docs/version-2.2.0/login-with-link/intro.md new file mode 100644 index 00000000..acbd1ec6 --- /dev/null +++ b/documentation/versioned_docs/version-2.2.0/login-with-link/intro.md @@ -0,0 +1,9 @@ +--- +id: intro +title: Introduction +slug: discourse-plugin/login-with-link +--- + +The Lexicon Discourse plugin provides support for integrating Discourse's email login with your Lexicon-powered mobile app. Our plugin modifies links in Discourse login emails so that when a relevant link is tapped, and the user has your Lexicon-powered mobile app installed, it will open the app and automatically log the user in. + +This section of the documentation offers step-by-step instructions to integrate this login functionality into your Discourse site, providing your users with a more seamless experience with your Lexicon-powered mobile app. diff --git a/documentation/versioned_docs/version-2.2.0/login-with-link/setup/enable-login-with-link.md b/documentation/versioned_docs/version-2.2.0/login-with-link/setup/enable-login-with-link.md new file mode 100644 index 00000000..f134ac93 --- /dev/null +++ b/documentation/versioned_docs/version-2.2.0/login-with-link/setup/enable-login-with-link.md @@ -0,0 +1,25 @@ +--- +title: Enabling login with link +--- + +import useBaseUrl from '@docusaurus/useBaseUrl'; + +This guide will walk you through the necessary steps to activate login with link at lexicon app on your Discourse site. + +## Steps + +1. Access your Discourse admin dashboard. + +2. Navigate to the `Plugins` section. + + + +3. Locate the `discourse-lexicon-plugin` and click on the `Settings` button. + +4. Fill in the `lexicon app scheme` setting with your app scheme. The app scheme is required to enable login with linking. + +5. Check the `lexicon login link enabled` box in the Lexicon settings section and save your changes. + + + +Once the login with link feature is enabled, you will be able to utilize its functionality in your Discourse instance. diff --git a/documentation/versioned_docs/version-2.2.0/login-with-link/setup/verify-login-with-link.md b/documentation/versioned_docs/version-2.2.0/login-with-link/setup/verify-login-with-link.md new file mode 100644 index 00000000..50657a0b --- /dev/null +++ b/documentation/versioned_docs/version-2.2.0/login-with-link/setup/verify-login-with-link.md @@ -0,0 +1,39 @@ +--- +title: Verify Login With Link +--- + +import useBaseUrl from '@docusaurus/useBaseUrl'; + +Below, we'll walk you through how you can validate the functionality of login with a link within your Lexicon-powered mobile app. + +:::note +The steps below assume that **you have already build your Lexicon-powered mobile app with the correct app scheme**. If you are running the app on your machine locally through Expo, these steps will not work. +::: + +:::info +In order to be able test login with a link, **you will need to use Lexicon version 2.2.0** for your Lexicon app. +::: + +## Steps + +To test login with a link within your Lexicon-powered mobile app, follow these steps: + +1. Ensure that you have Lexicon-powered mobile app at your device. +2. On your mobile device, open your Lexicon-powered mobile app and log in using one of your accounts. + - **Note**: Ensure that your email client on your mobile device will receive emails for this account. +3. On the login screen, enable `send login link, skip password`. Then, enter your Discourse email account and click the `send link` button. You will receive a popup message to check your email. + +
+ +
+4. Open your email on your phone and check the email sent by your Discourse website. +5. Click the link provided in the email. + +
+ + +
+ +6. The link will first open in your mobile web browser. If the Lexicon-powered mobile app is installed and matches the configured app scheme, it should automatically log you in to your app. + +And that's it! The Lexicon Discourse plugin will properly log you in with a link through your Discourse site. diff --git a/documentation/versioned_docs/version-2.2.0/optimal.md b/documentation/versioned_docs/version-2.2.0/optimal.md new file mode 100644 index 00000000..789e1674 --- /dev/null +++ b/documentation/versioned_docs/version-2.2.0/optimal.md @@ -0,0 +1,98 @@ +--- +title: 'Optimal Experience' +--- + +If you're planning to make use of the Lexicon Mobile App, there are a few adjustments you should make to your Discourse instance to provide the best in-app experience to your users. + +## Install the Lexicon Discourse Plugin + +The Lexicon Discourse plugin enhances the native mobile experience for your users in two key ways: + +- Adds support for push notifications +- Adds support for email deep linking. + +You can read more about the plugin and how to set it up [here](./discourse-plugin.md). + +## Enable Topic Excerpts + +We have designed the Mobile App so that users can easily see the first few sentences of a topic as they scroll through the topics list. + +However, by default, Discourse does not return excerpts when listing topics. + +Fortunately, there is a secret setting that enables this. + +It just takes a bit of additional configuration to enable. + +While Discourse does enable opting into this behavior as part of a [Theme Component](https://meta.discourse.org/t/topic-list-excerpts-theme-component/151520), we wanted to guide you through the option of toggling the setting itself. + +Should you prefer to enable it using the above theme component, you're free to do so. + +Enabling this setting involves gaining access to the server and changing a setting. + +### Instructions + +The original instructions can be found [here](https://meta.discourse.org/t/discourse-as-a-simple-personal-blog-engine/138244/4). + +Once you've gained access to your server, enter into the running Discourse app. + +```sh +$ /var/discourse/launcher enter app +``` + +Next, enter the Rails CLI: + +```sh +$ rails c +``` + +Finally, set the setting to true: + +```sh +$ SiteSetting.always_include_topic_excerpts = true +``` + +After that, you can exit, and excerpts should now be displaying in the app. + +## Enable Topic Tagging + +The Lexicon Mobile App was designed with the ability to tag topics in mind. + +This allows users to view and manage tags on topics, which is a popular feature on many Discourse servers. + +Unfortunately, this is not enabled by default. + +### Instructions + +In order to enable it, you can take the following steps: + +- Navigate to the Admin Site Settings page at `/admin/site_settings` +- Use the search bar to search for the setting `tagging enabled` +- Ensure that it is checked +- If you made a change, click the green checkbox button to apply it + +Topics should now be taggable, and viewable in the app. + +## Configure Upload Extensions + +Discourse provides a security feature that allows Discourse admins to specify a whitelist of file extensions that their users can upload. +For example, most admins would choose to restrict uploading of `.exe` files. +In order to be compatible with the settings of your Discourse instance, the Lexicon Mobile App simply requests the list of allowed extensions and uses it to enforce allowed extensions in the app. +Out of the box, most Discourse instances support this default list of extensions: + +- `.jpg` +- `.jpeg` +- `.png` +- `.gif` +- `.heic` +- `.heif` + +If you'd like to adjust the list of extensions in your Discourse instance, you can do so by following the instructions below. + +### Adjusting Allowed Extensions in Discourse + +- Navigate to the Admin Site Settings page at `/admin/site_settings` +- Use the search bar to search for the setting `extensions` +- Find the setting labeled `authorized extensions`. +- Adjust the list as you see fit to include the file extensions you'd like your users to be able to upload. +- When you are done making changes, click the green checkbox to apply them. +- The Lexicon Mobile App will receive the updated list of extensions from your site settings and begin enforcing it for your users. diff --git a/documentation/versioned_docs/version-2.2.0/play-store.md b/documentation/versioned_docs/version-2.2.0/play-store.md new file mode 100644 index 00000000..f329954e --- /dev/null +++ b/documentation/versioned_docs/version-2.2.0/play-store.md @@ -0,0 +1,168 @@ +--- +title: Publishing to the Play Store +--- + +import useBaseUrl from '@docusaurus/useBaseUrl'; + +## Prerequisites + +:::note +If you don't already have a Google Developer account, note that there is a fee to create one. +::: + +- A [Google Developer Account](https://play.google.com/console/signup) to access the [Google Play Console](https://play.google.com/console) +- An Expo account +- EAS CLI 2.6.0 or newer +- The [Lexicon Discourse plugin](./discourse-plugin.md) is already installed on your Discourse instance + +## Google Play Console + +The [Google Play Console](https://play.google.com/console) enables you to setup your app, invite beta testers, and publish your app to the [Google Play Store](https://play.google.com/store). + +Because you're publishing an app that was built using Expo, it is **very important** that you follow [Expo's instructions](https://github.com/expo/fyi/blob/master/first-android-submission.md) for submitting an app to the Google Play store correctly. + +## App Configuration + +After setting up your app in the Google Play Console, there are some other adjustments you'll need to make. + +### Build Config + +Similar to the approach for [Publishing to the App Store](app-store), if you haven’t already, you'll need to set your app name and slug in `frontend/app.json`. The [slug](https://docs.expo.dev/workflow/glossary-of-terms/#slug) is used as part of the URL for your app on Expo's web services, so it is recommended to use kebab-case (e.g., `my-lexicon-app`). + +Replace these placeholders with your desired values: + +:::info +Note below that `scheme` is included. If you want [email deep linking](./email-deep-linking/intro.md) support in your app, **you must specify a scheme**, and then configure the Lexicon Discourse plugin with the same scheme. +::: + +```json +"name": "", +"slug": "", +"scheme": "", +``` + +Then, you need to configure EAS Build by running the following command, or skip to the next [step](play-store#setup-config-values): + +```bash +eas build:configure +``` + +The EAS CLI will prompt you to specify `android.package` and `ios.bundleIdentifier` if those values are not already provided in `app.json`. + +Next, verify that the `package` name and other details specific to your app are included in the `android` section of `app.json`. Note that the `versionCode` will be automatically updated when you build the app with the `production` profile, so you don't need to increment the version manually. + +Also, there's one further detail that you might want to add, depending on your app's permissions. + +In the example below, we're providing our app with the ability to read and write to external storage. + +```json + "android": { + "package": "", + "permissions": [ "READ_EXTERNAL_STORAGE" , "WRITE_EXTERNAL_STORAGE" ] + "versionCode": 1, + }, +``` + +If your app requires further permissions, be sure to specify them as needed in this part of the configuration. + +If you don't quite understand how permissions work yet, it's best to check out the [Expo documentation](https://docs.expo.io/versions/latest/sdk/permissions) on this topic in order to get a full understanding. + +### Setup Config Values + +:::info +When publishing your app, it is necessary to deploy Prose somewhere publicly accessible, perhaps on a cloud hosting provider like AWS or DigitalOcean. If Prose is only running on your local machine, users that download your app won't be able to use it. +Check [the documentation](deployment) to deploy Prose if you haven't already. +::: + +Next, set the **Prose URL** for your builds in `Config.ts`. You can set a different URL for each build channel. + +:::note +In the original release of Lexicon, the **Prose URL** was specified in `frontend/.env`. However, as part of migrating to Expo's EAS feature, we centralized the configuration into `frontend/Config.ts` to save you the trouble of needing to maintain it in more than one place, as suggested in the [Expo documentation](https://docs.expo.dev/build-reference/variables/#can-i-share-environment-variables-defined-in-easjson-with-expo-start-and-eas-update) +::: + +```ts +const config = { + // ... + buildChannels: { + preview: { + proseUrl: 'http://PLACEHOLDER.change.this.to.your.prose.url', + }, + production: { + proseUrl: 'http://PLACEHOLDER.change.this.to.your.prose.url', + }, + }, +}; +``` + +### Add the Play Store Secret File + +For the last step, you'll need to provide a `.json` file containing a private key in order to interact with the Play Store. Follow [this guide](https://github.com/expo/fyi/blob/main/creating-google-service-account.md) to generate one. Then, copy the JSON file to your `lexicon/frontend` directory, and rename the file as `playstore_secret.json`. + +The JSON file looks like this: + +```json +{ + "type": "service_account", + "project_id": "", + "private_key_id": "", + "private_key": "-----BEGIN PRIVATE KEY----------END PRIVATE KEY-----\n", + "client_email": "", + "client_id": "", + "auth_uri": "", + "token_uri": "", + "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", + "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/lexicon%40api.iam.gserviceaccount.com" +} +``` + +## Build your App for Android + +Because we're working with Expo and React Native, this step isn't too different from building your app for iOS. + +From the `frontend/` directory, you can run this command to check the app before publishing: + +```bash +eas build --platform android --profile preview +``` + +Running `eas build` with the `preview` profile will build the app as an APK. This allows you to quickly load it onto your Android device or emulator. After the build is done, navigate to your project in the [Expo web console](https://expo.dev), then click on the **Builds** menu located on the left-hand side of the screen. + +- Click on the project you want to install. + + Builds + +- Download the app by pressing the `Install` button in the `Build Artifact` section. + + Build Artifact + +You can download and launch the app on your real device, or drag the downloaded APK file to your emulator. + +Once you have verified that the app runs as expected, you can proceed to build it for release: + +```bash +eas build --platform android --profile production +``` + +The approach for a production build is similar to the one used for generating a preview build. However, unlike a preview build, you won't be able to launch the production build in Android emulator—it is intended solely for publishing to the Play Store. + +Once this process is completed, you can proceed with submitting it to the Play Store. + +## Publish to the Play Store + +At this point, you can take your app live on the Google Play Store, or you can proceed with internal testing on the Google Play Console. + +To proceed with internal testing, run this command: + +```bash +eas submit --platform android --profile staging +``` + +To release your app publicly, run this command: + +```bash +eas submit --platform android --profile production +``` + +You can read more about build profiles [here](tutorial/publishing). + +At this point, provided that you've completed all the steps, congratulations! Your Lexicon-powered mobile app is now live and ready to be downloaded by your users. diff --git a/documentation/versioned_docs/version-2.2.0/publish-app.md b/documentation/versioned_docs/version-2.2.0/publish-app.md new file mode 100644 index 00000000..267f3e1a --- /dev/null +++ b/documentation/versioned_docs/version-2.2.0/publish-app.md @@ -0,0 +1,11 @@ +--- +title: Publishing your App +--- + +:::danger Progress +This page has not been started yet or needs a lot more work. +::: + +Expo workflow, benefits of, etc. + +Over the air updates? diff --git a/documentation/versioned_docs/version-2.2.0/push-notifications/introduction.md b/documentation/versioned_docs/version-2.2.0/push-notifications/introduction.md new file mode 100644 index 00000000..619429c6 --- /dev/null +++ b/documentation/versioned_docs/version-2.2.0/push-notifications/introduction.md @@ -0,0 +1,8 @@ +--- +title: Introduction +slug: /push-notifications +--- + +The Lexicon Discourse plugin provides support for native push notifications for your Lexicon-powered mobile app. This works for both Android and iOS, and is handled by Expo's [push notifications service](https://docs.expo.dev/push-notifications/overview/). + +This documentation offers step-by-step instructions to seamlessly integrate push notifications into your Discourse site so that your users receive them in your Lexicon-powered mobile app. By following this guide, you will be able to enhance the UX of your users by ensuring they receive timely and engaging notifications about activity on your Discourse site. diff --git a/documentation/versioned_docs/version-2.2.0/push-notifications/plugin-interaction.md b/documentation/versioned_docs/version-2.2.0/push-notifications/plugin-interaction.md new file mode 100644 index 00000000..5a424ac4 --- /dev/null +++ b/documentation/versioned_docs/version-2.2.0/push-notifications/plugin-interaction.md @@ -0,0 +1,31 @@ +--- +title: How Push Notifications work with Lexicon +--- + +import useBaseUrl from '@docusaurus/useBaseUrl'; + +Below, we outline the interaction between the Lexicon mobile app, Prose, and the Discourse Plugin regarding the implementation of push notifications. + +## The Lexicon mobile app + +The Lexicon mobile app plays a crucial role in enabling push notifications for your users. When a user logs into their account using the app, a unique token is generated using the [`expo-notifications`](https://docs.expo.dev/versions/latest/sdk/notifications/) library. This token serves as a unique identifier for the user's device. The app then sends this token to the Prose GraphQL API, which makes a separate request to the Lexicon Discourse plugin. The plugin then inserts a record into your Discourse site's database—ensuring any relevant activity on Discourse triggers a push notification to the user's mobile device. + +## Prose + +As mentioned elsewhere in the documentation, Prose is an intermediary component that facilitates communication between the Lexicon mobile app and your Discourse site. It serves the key role of providing a GraphQL interface over Discourse, which allows the mobile app to communicate with Discourse via GraphQL. + +The latest Prose API exposes a new GraphQL mutation, `pushNotifications`, to receive the unique Expo push token from the mobile app when the user logs in. + +Once Prose receives the token from the app, it forwards the token to the Discourse Plugin running on your site. + +## Discourse Plugin + +The Lexicon Discourse Plugin provides several features. In terms of enabling push notifications, it is responsible for integrating with Expo's [push notifications service](https://docs.expo.dev/push-notifications/overview/). When the Discourse Plugin receives a push token from Prose, it saves the token in your Discourse site's database, associating it with the corresponding user. + +Since the Lexicon Discourse plugin has been configured to respond to events within your Discourse site, it is able to dispatch push notifications based on your users' activity. + +When a relevant event triggers the need for a push notification, such as a new message or reply, the Discourse Plugin retrieves the associated user's token from your Discourse site's database. Using this token, the plugin sends a push notification request to Expo's push notification service, triggering the delivery of the push notification to the user's device. + +## Flowchart + +Build Artifact diff --git a/documentation/versioned_docs/version-2.2.0/push-notifications/setup/enable-push-notifications.md b/documentation/versioned_docs/version-2.2.0/push-notifications/setup/enable-push-notifications.md new file mode 100644 index 00000000..7acc5b98 --- /dev/null +++ b/documentation/versioned_docs/version-2.2.0/push-notifications/setup/enable-push-notifications.md @@ -0,0 +1,32 @@ +--- +title: Enable Push Notifications +--- + +import useBaseUrl from '@docusaurus/useBaseUrl'; + + + + + + +Below, we'll walk you through the necessary steps to activate push notifications for your Discourse site. + +## Steps + +1. Ensure the [Lexicon Discourse plugin](../../discourse-plugin-installation.md) is installed and activated. + +1. As an admin user, access your Discourse admin dashboard. + +1. Navigate to the Plugins section. + + + +4. Click on the `Settings` button for the `discourse-lexicon-plugin` entry. + +5. Check the `enable Push Notifications` box in the Lexicon settings section and save your changes. + + + +Once the push notifications setting is enabled, your users will be able to login through the mobile app and start receiving push notifications. + +It is important to remember that push notifications are setup specifically when the user logs in through the mobile app. If users are not receiving push notifications, you should instruct them to log out and log back in before attempting any further troubleshooting. diff --git a/documentation/versioned_docs/version-2.2.0/push-notifications/setup/verify-push-notifications.md b/documentation/versioned_docs/version-2.2.0/push-notifications/setup/verify-push-notifications.md new file mode 100644 index 00000000..7cfe6654 --- /dev/null +++ b/documentation/versioned_docs/version-2.2.0/push-notifications/setup/verify-push-notifications.md @@ -0,0 +1,33 @@ +--- +title: Verify Push Notifications +--- + +import useBaseUrl from '@docusaurus/useBaseUrl'; + + + + + +Below, we'll walk you through how you can validate the functionality of push notifications within your Lexicon-powered mobile app. + +:::info +In order to properly test push notifications, **you will need two separate accounts** on your Discourse site (to generate notifications). + +Additionally, **you will need at least one mobile device** for testing purposes. +::: + +## Step + +To test push notifications within your Lexicon-powered mobile app, follow these steps: + +1. Ensure that you have completed the [Getting Started](../../quick-start) steps for Lexicon. +1. Start the Lexicon Expo app by navigating to `frontend/` and running `yarn start` from your terminal. +1. Using the Expo link or QR Code, launch the app on a real mobile device. +1. Login to the app using one of your accounts. +1. Using that account, create a post within your Discourse site +1. Using a separate account, reply to the post to trigger a notification for the first account. +1. You should receive a push notification on your phone with the reply content from the other account. + + + +And that's it! The Lexicon Discourse plugin is properly sending push notifications through your Discourse site. diff --git a/documentation/versioned_docs/version-2.2.0/quick-start.md b/documentation/versioned_docs/version-2.2.0/quick-start.md new file mode 100644 index 00000000..89f4c8da --- /dev/null +++ b/documentation/versioned_docs/version-2.2.0/quick-start.md @@ -0,0 +1,65 @@ +--- +title: Quick Start +--- + +## Prerequisites + +- Node.js 16.14 or newer +- The latest version of NPM or Yarn, compatible with Node 16.14 or newer +- EAS CLI 3.7.2 or newer to build and publish the app +- An active Discourse site + - If you don’t have one, please follow the instructions in [Development Setup](setup#discourse-host) + +:::note +Follow the instructions in [Setup Guidance](tutorial/setup) to install the prerequisite depedencies, such as NPM and the EAS CLI. +::: + +## Installation + +Clone the repository and navigate into it: + +``` +git clone git@github.com:lexiconhq/lexicon.git +cd lexicon +``` + +Next, install the project's dependencies and generate its GraphQL schema: + +``` +$ npm install && npm run generate +``` + +Note that `npm run generate` involves two steps. + +- First, it will generate a [GraphQL schema](https://nexusjs.org/docs/guides/schema) in the `api` directory. + +- Then, using the generated schema, it will create a new folder called `generated` in the `frontend` directory, containing the resulting query and mutation types. + +- This allows the frontend codebase to stay in sync with, and not duplicate the code for, the types from the `api` directory. + +The code shared from the API is then used by [Apollo](https://github.com/apollographql/apollo-tooling), the GraphQL library we use on the frontend, which enables the Mobile App to query the API correctly. + +## Launch the Mobile App + +You can run the app and test it out by running this command from the project root: + +``` +$ npm run quickstart +``` + +This will simultaneously launch two processes: + +- The Prose GraphQL API Server +- The local Expo dev server, which will enable you to launch the React Native app from your device + +**Please note that this takes some configuration to setup properly**. + +- The `quickstart` command configures the Mobile App and the Prose GraphQL API to point at https://meta.discourse.org, as an example. + +- You'll need to make adjustments to point at a site of your choice. + +- The Lexicon Mobile App (via Expo) must be configured to point at the Prose GraphQL Server + +- The Prose GraphQL Server must be configured to point at an active Discourse instance + +More details are available in the [Development Setup](setup) section diff --git a/documentation/versioned_docs/version-2.2.0/rationale.md b/documentation/versioned_docs/version-2.2.0/rationale.md new file mode 100644 index 00000000..bfb62fa4 --- /dev/null +++ b/documentation/versioned_docs/version-2.2.0/rationale.md @@ -0,0 +1,73 @@ +--- +title: Background & Motivation +--- + +### Discourse's Approach to a Mobile Experience + +Discourse is a phenomenal, battle-tested piece of software that facilitates thoughtful discussions in countless communities around the globe. It's no secret that we are big fans of it. + +The Discourse core team's strategy for mobile devices was to implement their product as a responsive website, and optimize for mobile use cases. This allowed mobile users to simply go to the same Discourse site as they would have on devices with larger screens— enabling them to view and write posts from their mobile devices. + +However, over time, interest in a dedicated Discourse mobile app grew. The core team addressed this need by building a native mobile app. They chose to reuse their existing work by having the app simply wrap a webview containing the mobile site. + +This was a nice improvement, as it allowed the mobile app to integrate with native SDKs and provide some additional features to Discourse mobile users. + +Overall, their approach to solving this problem was both efficient and well-done. + +However, it is still evident to many users that they're interacting with an embedded web browser, and it's clear that it's not a mobile-_first_ experience. + +For many users and site-owners, what the Discourse team has provided is more than enough, and it solves all of their problems. + +In our case, we were looking for a very specific type of experience. + +### Who We Are + +The Lexicon Team is part of [KodeFox](https://www.kodefox.com/), a software studio comprised of passionate software engineers, designers, and product managers who regularly build world-class software for our customers. + +Interested in custom software development with a personal touch? Drop us a line at [hello@kodefox.com](mailto:hello@kodefox.com). + +### Enter Lexicon + +Lexicon was formed out of the desire to further leverage many of the great features that the Discourse team had worked hard to build. + +In our consulting projects, we found that many of our clients were regularly asking for solutions that Discourse already provides out of the box. + +However, our clients wanted a seamless, native mobile experience, tailored to the brand that their users were already familiar with. + +After digging into the Discourse API documentation, we felt that it was worthy investment to build a mobile-first Discourse experience which also faciliated customizability. + +We were already fluent with the elegant development process provided by React Native and Expo, so it was a natural fit for us to build the mobile app with these technologies. + +This allowed us to achieve a high ratio of code reuse across iOS and Android, making feature implementations and bug fixes a much simpler process in most cases. + +In integrating with Discourse's API, we also noticed that the API documentation contains a disclaimer which encourages reverse-engineering to understand it. + +While we can appreciate the sentiment of figuring things out yourself, we wanted to provide an API experience that makes it easy for developers to dig into interactive documentation and quickly grasp the concepts. + +For this reason, we also chose to build Prose, our GraphQL API layer on top of the Discourse RESTful API. Another motivating factor was our existing fluency with GraphQL. + +This allowed us to quickly implement the mobile app with an intuitive API paradigm that we were already very familiar with. + +#### How Lexicon can help you + +If you already run an existing Discourse site and want a native mobile experience for your users, you can very quickly point Lexicon at your site and browse it in real-time from your device. + +Check out the [Quick Start](quick-start) page to see a rapid example of spinning up a mobile app for Discourse's own [Meta site](https://meta.discourse.org). + +But beyond that, Lexicon is an open source pre-built mobile app. This means that you can customize it to fit your brand. + +You can think of it like a template that you can use to build your own mobile app for your community. + +If you're interested in customizing the Lexicon Mobile app, you can learn more about that in the [White Labeling](white-labeling) section. + +And when you're finished, you can publish it to the Apple App Store or Google Play Store, which we cover in [Publishing your App](app-store). + +### FOSS Mindset + +Finally, while this project will benefit us and our clients in the future, we also wanted it to be a gift to the community. + +We recognize and support the culture of free and open-source software. That's why we're delighted to give back to the community in this way, just as the Discourse team originally did when they chose to open-source their hard work. + +So please engage with us on Github, and don't be shy about opening a new issue or even a PR. + +We look forward to working with you! diff --git a/documentation/versioned_docs/version-2.2.0/setup.md b/documentation/versioned_docs/version-2.2.0/setup.md new file mode 100644 index 00000000..99305a33 --- /dev/null +++ b/documentation/versioned_docs/version-2.2.0/setup.md @@ -0,0 +1,376 @@ +--- +title: Development Setup +--- + +### Clone the Lexicon Repository + +If you haven't already, make sure you [clone the Lexicon repository](quick-start#installation) from Github. + +### Setup a Discourse Instance, if necessary + +In order to get started developing against the Lexicon Stack, you'll need a running Discourse instance. + +To recap, the Lexicon Stack consists of: + +- The Lexicon Mobile App +- The Lexicon Prose GraphQL API +- A running Discourse instance + +Without a Discourse instance, the Prose GraphQL API has nowhere to retrieve data from. And when the Prose GraphQL API can't retrieve any data, the Lexicon Mobile App won't be able to receive anything either. + +For detailed instructions on setting up a local development instance of Discourse, head over to the [tutorial](./tutorial/setup-discourse), which will walk you through the process. + +However, if you already have a deployed instance of Discourse, we'd recommend using that instead. + +### Install the Lexicon Discourse Plugin + +The Lexicon Discourse Plugin is a Discourse plugin that adds support for [push notifications](./push-notifications/introduction.md) and [email deep linking](./email-deep-linking/intro.md). + +You can install the plugin in your Discourse instance by following the instructions in the [Discourse plugin documentation](./discourse-plugin.md). + +For local development, you're only able to test out push notifications, as email deep linking requires a published app with a [valid app scheme](https://docs.expo.dev/versions/latest/config/app/#scheme). + +If you wish to develop against the plugin itself, you can clone the codebase [here](https://github.com/lexiconhq/discourse-lexicon-plugin.git). + +### Configuration + +The [Lexicon Stack](concepts#architecture-of-the-lexicon-stack) requires some configuration in order to properly interact with your Discourse server. + +This involves configuring both the backend GraphQL API, which interacts with your Discourse instance; as well as the frontend Mobile App, which interacts with the GraphQL API. + +The architecture of this setup is depicted in [Architecture of the Lexicon Stack](concepts#architecture-of-the-lexicon-stack). + +#### Backend GraphQL API Configuration + +The [Prose GraphQL API](concepts#prose-discourse-through-graphql) is fairly simple in terms of configuration. In the simplest case, it only needs to know where your Discourse instance is accessible at. + +It receives its configuration via a [`.env` file](https://www.codementor.io/@parthibakumarmurugesan/what-is-env-how-to-set-up-and-run-a-env-file-in-node-1pnyxw9yxj) in the root of the `api/` directory. + +Here is the simplest configuration of the `api/.env` file: + +``` +PROSE_DISCOURSE_HOST=https://meta.discourse.org +``` + +It is also worth noting that you can optionally configure the **Hostname** and **Port Number** that the Prose API server listens on, both of which default to **localhost** and **port 80**, respectively. + +``` +PROSE_DISCOURSE_HOST=https://meta.discourse.org + +# Instruct Prose to broadcast publicly instead of on localhost +PROSE_APP_HOSTNAME=0.0.0.0 + +# Instruct Prose to listen on port 8929 instead of the default port 80 +PROSE_APP_PORT=8929 +``` + +For a comprehensive list of all environment variables that can be used to configure Prose, check out [Prose Environment Variables](env-prose). + +#### Frontend Mobile App Configuration + +:::note +In the original release of Lexicon, the **Prose URL** was specified in `frontend/.env`. However, as part of migrating to Expo's EAS feature, we centralized the configuration into `frontend/Config.ts` to save you the trouble of needing to maintain it in more than one place, as suggested in the [Expo documentation](https://docs.expo.dev/build-reference/variables/#can-i-share-environment-variables-defined-in-easjson-with-expo-start-and-eas-update) +::: + +To configure the frontend mobile app, you'll first need to set your app name and slug in `frontend/app.json`. The [slug](https://docs.expo.dev/workflow/glossary-of-terms/#slug) is used as part of the URL for your app on Expo's web services, so it is recommended to use kebab-case (e.g., `my-lexicon-app`). + +Replace these placeholders with your desired values: + +```json + "name": "", + "slug": "", +``` + +Next, change the value of `proseUrl` in `frontend/Config.ts` to the URL of your Prose GraphQL API—whether local or already deployed somewhere. + +```ts +const config = { + localDevelopment: { + proseUrl: 'http://localhost:8929', + }, + buildChannels: { + preview: { + proseUrl: 'https://preview.myserver.com:8080/subpath', + }, + production: { + proseUrl: 'https://myserver.com/api/prose', + }, + }, +}; +``` + +`localDevelopment.proseUrl` will be used during development when you run the app using `npm run start` or `expo start`, whereas the specific value within `buildChannels` (e.g., `production.proseUrl`) will be used when actually building the app. + +#### Development Scenarios + +When developing locally, there are at least three scenarios that you may find yourself in. + +Depending on which one applies to you, the config values across `frontend/Config.ts` and `api/.env` may need to be set differently. + +##### Scenario 1: Existing Prose Deployment + +If you've already deployed the Prose GraphQL API to a host that is publicly reachable, you will have already setup `api/.env` with the proper values. + +In that case, `frontend/Config.ts` only needs updated to point at the deployed GraphQL API. + +For example: + +```ts +const config = { + localDevelopment: { + proseUrl: 'https://my-deployed-graphql.api', + }, + buildChannels: { + preview: { + proseUrl: 'https://my-deployed-graphql.api', + }, + production: { + proseUrl: 'https://my-deployed-graphql.api', + }, + }, +}; +``` + +In the example above, we have configured the app to point at `https://my-deployed-graphql.api` in all scenarios, including during development when running with `npm run start`. + +##### Scenario 2: Run Prose Locally & Access from a Simulator + +:::info +If you are running the Prose server locally, you should not expect that the mobile app will continue to function if you turn off your development machine. You must **deploy** the server before attempting to use the mobile app without depending on your development machine. +::: + +This approach involves running both the Lexicon Mobile App and the Prose GraphQL API on your development machine. It is accomplished by instructing Expo to launch the Mobile App in the Android or iOS simulator. + +When developing this way, you can simply set `localDevelopment.proseUrl` to `http://localhost` in `frontend/Config.ts`. And then in `api/.env`, you can set `PROSE_APP_HOSTNAME` to `0.0.0.0`. + +Note that if you want to run Prose locally on a specific port, you would need to make sure that the configuration in both `api/.env` and `frontend/Config.ts` reflect that correctly. + +:::caution +If you configure `PROSE_APP_HOSTNAME` in `api/.env` to only listen on `localhost` or `127.0.0.1` (rather than `0.0.0.0`), it prevents others on the same network as your development machine from accessing it. This includes both your mobile device and the Android simulator, which can lead to connectivity issues when developing locally. +::: + +##### Scenario 3: Run Prose Locally & Access from your Mobile Device + +It can be very useful to develop and debug against the app using your actual mobile device with the [Expo Go app](https://expo.dev/client). + +In order to do this, you'll need to have your development machine reachable from your mobile device. + +A simple way to make it reachable is to ensure that your mobile device and development machine are on the same network, and then, in `api/.env`, set `PROSE_APP_HOSTNAME` to `0.0.0.0`. + +In a regular Expo project, you would be required to update the `localDevelopment.proseUrl` value in `frontend/Config.ts` to contain the hardcoded IP address of your development machine on your network. + +However, by setting the value to `http://localhost`, we handle this **automatically** by default, so you don't have to worry about it. Read more about it [here](env-mobile#infer_development_host). + +###### Hardcoding your local IP Address + +:::info +This approach is not ideal. If your local IP address ever changes, you'll need to locate it again, and update `Config.ts` to reflect that. For this reason, it's preferable to just use `http://localhost`. +::: + +To manually instruct the Mobile App how to locate your development machine, you'll need to find out what the **local IP address** of your development machine is on your current network. + +Note that your local IP address is different from your public IP Address. + +If you are not sure how to get your local IP address, you can go to [What Is My Browser: Detect Local IP Address](https://www.whatismybrowser.com/detect/what-is-my-local-ip-address) and follow the instructions. + +The website itself may not be able to automatically detect your local IP address, but it will give you instructions on how to locate it within your specific operating system. + +You will be given an IP address like `10.0.12.121` or `192.168.17.69`. + +You can then update the value in `frontend/Config.ts` to your local IP address. + +This will allow the app running on your mobile device to properly locate the GraphQL API running on your development machine. + +## Configure your Discourse Host + +As mentioned above, you'll need to have setup a Discourse host for the GraphQL API to interact with. + +We'd like to briefly cover the different approaches to setting up a Discourse Host for development before continuing. + +**1. Run a Discourse Instance Locally** + +:::note +Ensure that you are managing all of your ports correctly. + +The development setup of Discourse with Docker makes use of multiple ports, one of which being **port 3000** by default. You'll want to double-check that none of the environment variables are pointing at the ports Discourse is using. +::: + +If you'd like to run a Discourse site for development locally, the recommended way to do this to use **[Docker](https://www.docker.com/)**, so make sure you have it installed. + +Then, as we mentioned above, you can follow [these steps in the tutorial](tutorial/setup-discourse) to install and run a development instance of Discourse in Docker. + +**2. Use try.discourse.org or another popular Discourse site** +:::info +Feel free to use existing public Discourse sites—such as the [Docker Community Forum](https://forums.docker.com/) or the [Rust Programming Language Forum](https://users.rust-lang.org/)—in order to test out the Lexicon Mobile App. + +Just be mindful of how you're contributing to those sites if you do. +::: + +[Try Discourse](https://try.discourse.org/) is a publicly accessible Discourse instance which is intended for testing. As such, it resets every day. + +The only drawback of this approach is that you can only register as a normal user, and therefore cannot modify the site's admin settings. + +With this approach, you'd simply configure Prose in `api/.env` to point `PROSE_DISCOURSE_HOST` at one of these instances. + +```bash +PROSE_DISCOURSE_HOST=https://try.discourse.org +``` + +## Working with the Codebase + +Now that you've prepared everything for development, you can start digging in on the Lexicon codebase. + +### Run the Lexicon Mobile App & Prose GraphQL Server + +You can run the Mobile App and test it out with a local Prose server by running this command **from the project root**: + +``` +$ npm run dev +``` + +This will simultaneously launch two processes: + +- The GraphQL API Server +- The local Expo dev server, which will enable you to launch the React Native app from your device + +However, if you wish to run the frontend and backend seperately, execute the following command in a terminal to run the frontend + +``` +$ npm run --prefix frontend start +``` + +Then execute the following line in another terminal to run the backend + +``` +$ npm run --prefix api dev +``` + +### Debugging + +- Use [Expo Developer Menu](https://docs.expo.io/workflow/debugging/#developer-menu) to make the debugging process easier. + +Opening the Expo Developer Menu depends on your device: + +- On an iOS Device: Shake the device, or touch 3 fingers to the screen. +- On the iOS Simulator: Hit `⌘ + ctrl + Z` on a Mac in the emulator. +- On an Android Device: Shake the device vertically, or run `adb shell input keyevent 82` in the terminal window if the device is connected via USB. +- On the Android Emulator: Hit `⌘ + M`, or run `adb shell input keyevent 82` in your terminal window. + +- If your changes don't show up, it could involve a cache issue. In this case, you should try restarting Expo. + - To do so, quit the process by hitting `Ctrl + C` in the Terminal where it is running. + - Then run `npm run start` again. + - If the issue persists, you should look for the latest guidance from Expo on how to clear the cache, as it has been known to change. + +### Running the Test Suites + +Before running tests, double-check that your changes don't contain any errors. + +You can run tests across both the frontend and backend codebases sequentially by running the following command from the project root: + +``` +$ npm run test +``` + +On top of ensuring that all tests have passed, the command will also notify you if there are any Typescript errors or issues from Prettier or ESLint. + +Also note that the process of running `npm run test` triggers an additional action in the frontend to take place before running the tests. + +A new folder, `frontend/generated`, is created and populated with all the GraphQL Query and Mutation types for use in the codebase. + +If we did not run this before the tests, they would fail due to type errors. + +### Build & Publish the Lexicon Mobile App + +:::note +An Expo account is required in order to use Expo's services. You can create one here: https://expo.io/signup. +Once you have created your Expo account, please ensure that you are signed in with your current shell session, via `expo login` or `eas login`. +::: + +You are required to configure EAS build first by running: + +```bash +eas build:configure +``` + +You will then get a prompt from the EAS CLI related to the EAS project IDs: `android.package` and `ios.bundleIdentifier`. EAS will provide you with an existing project ID if you have one or ask you to create a new one. As for `android.package` and `ios.bundleIdentifier`, you can specify those values with `com.companyname.appname`, or any other patterns you might prefer. + +Once you're done, verify the `proseUrl` value you will use for the actual build of the app in `Config.ts`. + +:::info +When publishing your app, it is necessary to deploy Prose somewhere publicly accessible, perhaps on a cloud hosting provider like AWS or DigitalOcean. If Prose is only running on your local machine, users that download your app won't be able to use it. +Check [the documentation](deployment) to deploy Prose if you haven't already. +::: + +Now you can build the Mobile App via Expo (EAS) with the preview build profile by running command below: + +```bash +eas build –platform all –profile preview +``` + +When you do this, the packager will minify all your code and generate two versions of your code—one for iOS, and one for Android—and then upload them both to the Expo CDN. + +Additionally, if you haven't yet optimized the app's assets, Expo will ask you if you'd like to do so. + +This has the same effect as manually running `npx expo-optimize` beforehand. It simply compresses all of the image assets in your project to reduce the size of your build. + +When the process is complete, you'll be presented with a shareable QR Code and a URL resembling https://exp.host/@ccheever/an-example, which directs you to the build details in Expo's web console. + +At this point, anyone can then use that link to load your project. + +For Android, you can install the app on an emulator or on your physical device. However, for iOS, you can only install it on the iOS simulator. To run the app on a real iOS device, follow the steps in [this part](tutorial/building#1-preview) of the tutorial. + +When building your app, it is recommended to build it as a preview build first, and make sure everything runs well before building it for release with the production profile. + +To build the app with the production build profile, run this command: + +```bash +eas build –platform all –profile production +``` + +You will also be presented with links directing you to the build details in Expo. + +However, unlike the preview build, the release build cannot be installed directly on your physical device or in an emulator / simulator. You'll need to publish the app and then install it from either the Play Store or App Store. + +You can read a more detailed explanation of this process in [this section](tutorial/building) of the tutorial. + +#### Updates + +If you later want to deploy an update to your version of the Lexicon Mobile App, you can use the EAS update command. + +First, make sure to configure EAS update by running the following command: + +```bash +eas update:configure +``` + +This command will automatically add the `expo.runtimeVersion` field to your `app.json` file. +You'll see a warning in your terminal telling you to add `expo.updates.url` to `app.json`. + +Then run this command to update your project: + +```bash +eas update -–branch +``` + +:::note +The channel name is the same as the build profile, so for the preview builds, you can run: + +```bash +eas update -–branch preview +``` + +::: + +Read more about updating your app [here](tutorial/updating). + +Once published, the new version will be available to your users the next time they open it. + +For more details on this process—including publishing to the App Store and Google Play Store—follow the instructions in [Publishing your App](tutorial/publishing). + +#### Configure the GraphQL API with your Discourse Server + +In order for a published version of the app to be able to contact your Discourse server, you'll need to ensure that: + +- The GraphQL API is deployed and running properly on a host that is reachable from the app itself. +- The GraphQL API is configured to point at the correct host and port of your Discourse server +- Your Discourse server is reachable by the GraphQL API diff --git a/documentation/versioned_docs/version-2.2.0/supported-devices.md b/documentation/versioned_docs/version-2.2.0/supported-devices.md new file mode 100644 index 00000000..84e6b1bb --- /dev/null +++ b/documentation/versioned_docs/version-2.2.0/supported-devices.md @@ -0,0 +1,33 @@ +--- +title: Supported Devices +--- + +import useBaseUrl from '@docusaurus/useBaseUrl'; + +## iPhone and Android Phones + +:::info +Older versions of iOS and Android may work, but are not officially supported. +::: + +Once you've published to the App Store and Google Play Store, your published app will work out of the box for your users on both iPhone and Android devices with the following specifications: + +| Device | Minimum OS | +| --------------- | -------------------- | +| iPhone | iOS 16 and above | +| Android Devices | Android 13 and above | + +| Android | iOS | +| -------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | +| | | +| | | +| | | +| | | + +## Support for Other Devices + +At this time, **tablets - including iPads** - and other mobile devices are **not supported**. + +We may consider developing support for this in the future. + +If this is critical for you, please drop us a line at support@kodefox.io and let us know. diff --git a/documentation/versioned_docs/version-2.2.0/technologies.md b/documentation/versioned_docs/version-2.2.0/technologies.md new file mode 100644 index 00000000..8657288a --- /dev/null +++ b/documentation/versioned_docs/version-2.2.0/technologies.md @@ -0,0 +1,21 @@ +--- +title: Technologies +--- + +### 100% React Native and TypeScript built on Expo + +Lexicon was built, and is maintained, with a single code base—meaning that bug fixes, improvements, and new features will (in most cases) automatically apply to both iOS and Android. + +### GraphQL-based API + +Developers who wish to contribute to (or fork) Lexicon can do so with all the benefits of GraphQL. For more information, check out [Concepts and Architecture](concepts#prose-discourse-through-graphql). + +### White Labeling Support + +White Label the Lexicon Mobile App to give your users the familiar look and feel of your brand. Learn more in [White Labeling](white-labeling). + +### Painless integration with existing Discourse instances + +Getting started is as easy as spinning up a new server for the Prose GraphQL API, and pointing it at your Discourse instance. No changes are required on your Discourse instance itself. + +Note: to enable features like [Push Notifications](./push-notifications) and [Email Deep Linking](./email-deep-linking/intro.md), you can install our [Discourse Plugin](./discourse-plugin.md). diff --git a/documentation/versioned_docs/version-2.2.0/theming.md b/documentation/versioned_docs/version-2.2.0/theming.md new file mode 100644 index 00000000..04dbc172 --- /dev/null +++ b/documentation/versioned_docs/version-2.2.0/theming.md @@ -0,0 +1,247 @@ +--- +title: Theming +--- + +:::note +This section will involve reading and modifying Typescript. If you get stuck, reach out to us. +::: + +Lexicon allows you to customize the default theme that the Mobile App provides. + +You can accomplish this by modifying the values in `frontend/src/constants/theme`, or in `frontend/src/theme`. + +There is a difference between the two, and they work in conjunction with one another. + +`frontend/src/constants/theme` defines the underlying base values of the theme. + +`frontend/src/theme` then imports those values, and uses them to compose the actual theme object used throughout the rest of the Mobile App. + +## Colors + +### Adjusting Base & Functional Colors + +There are 2 types of colors in the Mobile App: base colors and functional colors. + +Base colors are the underlying palette of the theme, whereas functional colors define specific use-cases of the base colors. + +For example, you might have noticed that the Mobile App features a nice, eye-catching Royal Blue color as its primary color. + +This is defined in the base colors as: + +```ts +// ... +royalBlue: '#2B6AFF', +// ... +``` + +Then, the functional colors make use of this for particular components in the app. + +To continue with the example, the `royalBlue` base color is referenced in the functional colors as: + +```ts +// ... +activeTab: BASE_COLORS.royalBlue, +// ... +primary: BASE_COLORS.royalBlue, +// ... +``` + +Now, any component can reference the functional colors' `primary` value, and it will be `royalBlue`. + +However, if you wanted a different theme with a new color, such as, `BASE_COLORS.lightningYellow`, then you could adjust it to: + +```ts +// ... +activeTab: BASE_COLORS.lightningYellow, +// ... +primary: BASE_COLORS.lightningYellow, +// ... +``` + +And the Mobile App would replace the Royal Blue with the value you've defined for Lightning Yellow. + +For this reason, if you want to add more colors, you'll need to add base color values first, and then access them within the functional colors. + +This approach keeps a clean separation of concerns, which allows theme changes to seamlessly propagate throughout the Mobile App. + +### Color Scheme (Dark Mode and Light Mode) + +The theme allows you to control how the user can adjust the app's color scheme, if at all. + +There are three choices for this: `dark`, `light`, `no-preference`. + +- Dark: force the color scheme to remain dark +- Light: force the color scheme to remain light +- No Preference (default): allow your users to specify a preference for color scheme + +Note that if you specify `dark` or `light`, your users **will not** have the option of selecting a preference for color scheme. + +This manifests in the Mobile App by hiding the Dark Mode button which normally appears in the Preferences Scene. + +## Fonts + +The theme's fonts are declared in `frontend/src/constants/theme/fonts`. + +Inside of that file, you'll find multiple aspects of the fonts that can be adjusted: + +- Font Variants +- Font Sizes +- Heading Font Sizes + +### Font Variants + +Used to classify multiple font weights into named variants. It supports the following values: + +| Variants | Default font weight | +| -------- | ------------------- | +| bold | 700 | +| semiBold | 600 | +| normal | 400 | + +### Font Sizes + +Used to set a font size scale that is consistent throughout the app. It supports the following values: + +| Variants | Default size | +| ---------------- | ------------ | +| xl (extra large) | 24 | +| l (large) | 18 | +| m (medium) | 16 | +| s (small) | 14 | +| xs (extra small) | 12 | + +### Heading Font Sizes + +Used to classify multiple font sizes for heading elements, such as `h1`, `h2`, etc. + +These values are primarily used for rendering the content of posts and messages from Discourse. + +This is because Discourse posts are written in Markdown, and users will often leverage heading elements to format their posts. + +| Variants | Default size | +| -------------- | ------------ | +| h1 (Heading 1) | 32 | +| h2 (Heading 2) | 24 | +| h3 (Heading 3) | 22 | +| h4 (Heading 4) | 20 | +| h5 (Heading 5) | 18 | +| h6 (Heading 6) | 17 | + +## Icons + +The `icons` theme file is used to store icon-related constants. + +Currently, the ‘icons’ file only contains a constant which declares the icon sizes scale. + +| Variants | Default size | +| ---------------- | ------------ | +| xl (extra large) | 28 | +| l (large) | 24 | +| m (medium) | 20 | +| s (small) | 18 | +| xs (extra small) | 16 | + +## Images + +The `images` theme file is used to store theme constants used in rendering images. + +Currently, this file declares the following theme values: + +- Avatar Icon Size +- Avatar Letter Size +- Avatar Image Size + +Avatars are used throughout the app to display relevant info about a post or message. + +As such, it is typically the user's photo. + +However, when a photo is not provided, we also compose a letter-based avatar based on the user's initials. + +### Avatar Icon Size + +| Variants | Default size | +| ---------------- | ------------ | +| l (large) | 96 | +| m (medium) | 52 | +| s (small) | 40 | +| xs (extra small) | 28 | + +### Avatar Letter Size + +| Variants | Default size | +| ---------------- | ------------ | +| l (large) | 72 | +| m (medium) | 36 | +| s (small) | 28 | +| xs (extra small) | 16 | + +### Avatar Image Size + +This defines the quality of the image used for avatars. + +| Variants | Default size | +| ---------------- | ------------ | +| xl (extra large) | 450 | +| l (large) | 150 | +| m (medium) | 100 | +| s (small) | 50 | + +## Spacing + +The `spacing` theme file defines spacing constants used throughout the Mobile App for padding and margins. + +| Variants | Default size | +| ------------------------- | ------------ | +| xxxl (triple extra large) | 36 | +| xxl (double extra large) | 24 | +| xl (extra large) | 16 | +| l (large) | 12 | +| m (medium) | 8 | +| s (small) | 4 | +| xs (extra small) | 2 | + +## Advanced Customization + +While the above adjustments are generally fairly simple, you can really customize the Mobile App to your heart's content (based on your skill level). + +Here are some additional aexamples. + +### Custom Fonts + +#### Create a folder for the Custom Fonts + +To keep the codebase organized, create a folder named `fonts` inside of `frontend/assets`. + +You can then move your custom font assets into this folder. + +#### Install & Use the `expo-font` Package + +This package eases the process of adding custom fonts into an Expo-based app. + +In particular, you'll want to use the `loadAsync` function from it, which will map your font assets to their variant names throughout the Mobile App. + +While we won't get into too much technical detail here, their [documentation](https://docs.expo.dev/versions/latest/sdk/font/) can guide you through the process. + +### Error Messages + +It is possible to customize both the error messages and the means through which they are presented to the user. + +In order to do this, you should first be aware of two files. + +#### `frontend/src/helpers/errorMessage.ts` + +The Prose GraphQL API forwards on error messages from Discourse. + +This file declares the specific text of those messages as constants so that they can easily be compared in `errorHandler.ts`. + +If you observe any additional error messages that are not being caught, you'll want to add them to this file, and then adjust `errorHandler.ts` below accordingly. + +#### `frontend/src/helpers/errorHandler.ts` + +This file imports from the above `errorMessage.ts`. + +It then defines exactly how errors should be handled, including the above messages, when they are encountered. + +Currently, the default approach is to display the errors using an Alert to the user. + +However, if you wanted to integrate snackbars, you would adjust the code in `errorHandler.ts` to replace the invocations of `Alert.alert`. diff --git a/documentation/versioned_docs/version-2.2.0/troubleshooting-build.md b/documentation/versioned_docs/version-2.2.0/troubleshooting-build.md new file mode 100644 index 00000000..68d75dad --- /dev/null +++ b/documentation/versioned_docs/version-2.2.0/troubleshooting-build.md @@ -0,0 +1,128 @@ +--- +title: Troubleshooting when trying out the app +--- + + + + + +import useBaseUrl from '@docusaurus/useBaseUrl'; + +## Troubleshooting Connection and Configuration Issues with URL + +
+ please connect to network error +
+ +If you are encountering issues related to the URL, resulting in an error message saying "please connect to network" as shown in the screenshot, it is likely due to incorrect settings. Specifically, if you are attempting to test builds locally on your mobile device and the channel field is not properly configured, the app may continuously fallback to the localDevelopment channel, even if you have set it to something else like "preview." + +Here some steps and notes to help resolve this: + +- Open the `frontend/Config.ts` file in your project. +- Locate the `config` object within the file. +- In the `localDevelopment` section of the `config` object, you can add the Prose URL specific to the channel you are trying to test. This section is used for local development and as a fallback configuration for unknown build channels in EAS Build. Here's an example: + + ```ts + const config: Config = { + localDevelopment: { + proseUrl: 'http://localhost:8929', + }, + buildChannels: { + preview: { + proseUrl: 'http://PLACEHOLDER.change.this.to.your.prose.url', + }, + production: { + proseUrl: 'http://PLACEHOLDER.change.this.to.your.prose.url', + }, + }, + }; + ``` + +- The example above shows that the config consists of two main sections: localDevelopment, which specifies the URL during localDevelopment, and buildChannels, which includes configurations for different channels such as preview and production. For local development, it will hit the Prose API with the URL `http://localhost:8929`. If the buildChannel is unknown or not found, it will always default to localDevelopment. +- Update the `proseUrl` value within the desired build channel, such as `preview` or `production`, with the valid and reachable URL of your Prose server. +- Once you have made the necessary changes, save the `frontend/Config.ts` file. + +Now, when you run eas build for a specific build channel, such as `eas build --profile=production`, it will utilize the Prose URL specified in the production configuration. + +:::note +It is important to include the URL in the `frontend/app.json` file, which expo-updates will use to fetch update manifests. Failing to set the URL in the `frontend/app.json` file will result in the expo-update constant always returning undefined for the channel, causing the app to consistently utilize the localDevelopment URL after building. You can specify this URL in the expo and updates sections of the app.json file. For more detailed information on how to configure this, please refer to the [expo documentation](https://docs.expo.dev/versions/latest/config/app/#url) for more detail on this. + +```json +"expo": { + "updates": { + ..., + "url": "https://u.expo.dev/" + } +} +``` + +This configuration is essential for seamless integration with Config.ts in your project. +::: + +In certain cases, you may encounter an issue related to Prose API URLs when the channel name specified in the `frontend/eas.json` file does not match the corresponding key name defined in the `config` variable in `frontend/Config.ts`. This discrepancy can lead to problems because the channel name from `eas.json` is used to determine the URL that will be utilized. If the names do not match, the default `localDevelopment` URL will be used instead. + +To ensure smooth functioning, it is important to use the same channel name in both the `frontend/eas.json` file and the `frontend/Config.ts` file. This will ensure proper mapping of the channel name to the corresponding URL. + +Here is an example to illustrate this: + +```json +// frontend/eas.json + +"build": { + "staging": { + "android": { + "buildType": "apk" + }, + "channel": "staging" + } +} +``` + +```ts +// frontend/Config.ts; + +const config: Config = { + localDevelopment: { + proseUrl: 'http://localhost:8929', + inferDevelopmentHost: true, + }, + + buildChannels: { + preview: { + proseUrl: '', + }, + production: { + proseUrl: '', + }, + staging: { + proseUrl: '', + }, + }, +}; +``` + +## The app closes abruptly after the splash screen + +If you encounter a situation where your app closes abruptly after the splash screen, it is likely that there are missing configurations in your `app.json` file. One common cause is the absence of a scheme definition in `app.json`, which is essential during the app build process. + +To resolve this issue, follow these steps: + +1. Open your project's `frontend/app.json` file. +2. Look for the `"expo"` section. +3. If a scheme is not present add this part in `"expo"` section + +```json +"expo":{ + "name": "", + "slug": "", + "scheme": "", + "version": "1.0.0" +} +``` + +Replace `""` with the desired scheme name for your app. + +4. Save the changes to the `app.json` file. +5. Rebuild your app and test it again. + +By ensuring that the scheme is correctly defined in `app.json`, you should be able to resolve the issue of the app closing after the splash screen. diff --git a/documentation/versioned_docs/version-2.2.0/tutorial/building.md b/documentation/versioned_docs/version-2.2.0/tutorial/building.md new file mode 100644 index 00000000..3e261343 --- /dev/null +++ b/documentation/versioned_docs/version-2.2.0/tutorial/building.md @@ -0,0 +1,152 @@ +--- +title: Build your App +--- + +## EAS Build + +EAS Build is the upgraded version of `expo build`. This service helps to build app binaries for your Expo and React Native projects. Read more about it in the Expo documentation [here](https://docs.expo.dev/build/introduction/). + +### Configuration + +Let's get started by configuring EAS build. Check [here](https://docs.expo.dev/build-reference/build-configuration/) to see the complete guide from Expo. + +#### Build Setup + +Run this command in `/frontend` directory: + +```bash +eas build:configure +``` + +When running that command, the EAS CLI will typically do the following: + +1. It will prompt you for the EAS project ID, either to use an existing ID if you have one, or create a new one. Then it will automatically add the `expo.extra.eas.projectId` field in `app.json`. +2. It will create a new `eas.json` file if one doesn’t already exist. However, we have that set up for you, so you don't need to worry about creating one. 🎉 +3. It will prompt you to specify `android.package` and `ios.bundleIdentifier` if those values are not already provided in `app.json`. Note that those two values don't have to be the identical. + +You can see that the values in `app.json` are updated after running the command. + +#### Configuration Values + +:::info +When publishing your app, it is necessary to deploy Prose somewhere publicly accessible, perhaps on a cloud hosting provider like AWS or DigitalOcean. If Prose is only running on your local machine, users that download your app won't be able to use it. +Check [the documentation](../deployment.md) to deploy Prose if you haven't already. + +In the original release of Lexicon, the **Prose URL** was specified in `frontend/.env`. However, as part of migrating to Expo's EAS feature, we centralized the configuration into `frontend/Config.ts` to save you the trouble of needing to maintain it in more than one place, as suggested in the [Expo documentation](https://docs.expo.dev/build-reference/variables/#can-i-share-environment-variables-defined-in-easjson-with-expo-start-and-eas-update) +::: + +Next, open `Config.ts` and overwrite the placeholder values with the Prose URL you want to use for the build version. You can either set the same values or a different one for each channel. You don't need to adjust the values in `localDevelopment` since that is only used in development, and not when building the app. + +```ts +const config = { + // ... + buildChannels: { + preview: { + proseUrl: 'http://PLACEHOLDER.change.this.to.your.prose.url', + }, + production: { + proseUrl: 'http://PLACEHOLDER.change.this.to.your.prose.url', + }, + }, +}; +``` + +### Run a Build + +#### Build for Both Platforms + +To build on both platforms, you can use either of the commands below: + +```bash +eas build --platform all +``` + +```bash +eas build -p all +``` + +#### iOS only + +```bash +eas build --platform ios +``` + +#### Android only + +```bash +eas build --platform android +``` + +#### Run a build with a specific profile + +```bash +eas build --platform all –-profile +``` + +```bash +eas build -p all –e +``` + +:::note +Without --profile, the EAS CLI will default to the `production` profile. +::: + +### Build Profiles + +Build profiles serve as a way of grouping configuration values for different scenarios when building the mobile app. + +You can find more details [here](https://docs.expo.dev/build/eas-json/). + +The `eas.json` file can contain multiple build profiles. However, it typically has 3 profiles: **preview**, **development**, and **production**. + +#### 1. Preview + +Purpose: to internally test the app in production-like circumstances. + +It is recommended to try building with the preview profile **_first_** before building your app with the production profile. That way, you can ensure the app runs as expected before it’s ready to be published. + +The build type for Android will be an **APK** file, whereas the iOS build will output a format that can be installed on the simulator. + +This is because the `ios.simulator` option was specified in `eas.json`: + +```json + "ios": { + "simulator": true + }, +``` + +If you want to run the preview build on a real device, you'll need have an Apple account with Apple Developer Enterprise Program membership, then add the `ios.enterpriseProvisioning` value in `eas.json`: + +```json + "ios": { + "enterpriseProvisioning": "universal" + } +``` + +For the `preview` build profile, we have already set the distribution mode to [internal](https://docs.expo.dev/build/internal-distribution/). This ensures that EAS build provides shareable URLs for builds, with instructions on how to get them running. + +This approach then allows us to test the app without submitting to the App Store or Play Store. + +#### 2. Development + +Purpose: to make debugging easier. Expo will automatically include developer tools in the build. As you may have figured, this build should never be published to either of the app stores. + +Development builds depend on [expo-dev-client](https://docs.expo.dev/development/introduction/), so Expo will prompt us to install the library if needed. + +Similar to preview builds, you can add the iOS options mentioned above to run them on a simulator or real device. + +#### 3. Production + +Purpose: for submission to the App Store and Play Store—as a public release, or as part of testing in each respective ecosystem. + +In order to use builds like this, they must be installed through the respective app stores. + +After running builds with this profile, you'll see that the iOS and Android versions have automatically been incremented. As you might expect, this is because `autoIncrement` has been set to `true`. + +It is worth noting, however, that this behavior only applies to TestFlight and Internal Testing, so you'll need to be sure to also manually increment the `expo.version` in `app.json` for public release. Expo provides further [documentation](https://docs.expo.dev/build-reference/app-versions/) on this topic. + +## The App is Built + +Great work! You can now share the installation link with your peers so they can try out the app. + +In the next section, you'll learn how to [publish](publishing) your app to the App Store and Play Store! 🚀 diff --git a/documentation/versioned_docs/version-2.2.0/tutorial/install-prose.md b/documentation/versioned_docs/version-2.2.0/tutorial/install-prose.md new file mode 100644 index 00000000..d6ecec67 --- /dev/null +++ b/documentation/versioned_docs/version-2.2.0/tutorial/install-prose.md @@ -0,0 +1,360 @@ +--- +title: Setup the Prose GraphQL API +--- + +Now that we have a running Discourse instance to interact with, we can move onto setting up the Prose GraphQL Server. + +To recap, Prose is a part of the Lexicon stack. + +It is responsible for providing a [GraphQL](https://graphql.org/) interface on top of Discourse, which the Lexicon Mobile App can then interact with. + +For more information about this, check out [Concepts & Architecture](../concepts). + +## Approaches for Setting Up Prose + +If your Discourse instance is running locally, it is natural that you should also setup your Prose server locally. + +Otherwise, it would be unnecessary extra work to get a remote Prose server communicating with your local Discourse server. + +However, if you've setup your Discourse instance in the cloud, it is up to you if you want to run your Prose server locally or in the cloud as well. + +If you'd like to install it in the cloud, you'll want to setup an additional server - similar to how you would set one up for Discourse. If you're not yet comfortable with this, feel free to jump back to the page, [Setup a Cloud Server (Optional)](setup-cloud-server). + +Bearing all of that in mind, once you have identified where you'd like to host Prose, you should also consider how you'd like to install it onto that machine. + +The first way, which we recommend, is to use **[Docker](https://www.docker.com/)**. + +And of course, the second way is to install it manually, rather than using containers. + +## Install Prose using Docker + +The reason we recommend using Docker is because you won't have to worry about setting up Prose's on your machine. + +We have already published Prose to [Docker Hub](https://hub.docker.com/), which means you can easily pull it down and run it. We'll guide you through that below. + +### Install Docker + +First, just as was necessary for setting up Discourse, you'll want to make sure Docker is installed on your machine. + +You can follow the instructions on the [Docker installation page](https://www.docker.com/get-started) if you are unsure of how to do this. + +### Pull and Run the Prose GraphQL API Image + +After successfully installing Docker, you can use the command below to run the Prose GraphQL image. + +Just bear in mind that you'll want to adjust some of the **environment variables** to your situation before you run the command. + +``` +$ docker run -d \ + -e PROSE_DISCOURSE_HOST=https://meta.discourse.org \ + -e PROSE_APP_PORT=80 \ + -p 5000:80 \ + --name prose-graphql \ + kodefox/prose +``` + +The above command will take care of pulling the Prose GraphQL Docker Image, building it, and running it in a container. + +To help understand everything that's going on there, let's break it down line by line. + +```bash +docker run -d +``` + +This instructs Docker to run our image as a container in **detached mode**. This is similar to backgrounding a process. + +```bash +-e PROSE_DISCOURSE_HOST=https://meta.discourse.org +-e PROSE_APP_PORT=80 +``` + +The `-e` flag instructs Docker that we want to set or override certain environment variables in the container with the values we provided. + +In this case, we're telling Prose to interact with the Discourse instance is running at `https://meta.discourse.org`, and that Prose should run itself _inside of the container_ on a port of `80`. + +``` +-p 5000:80 +``` + +Next, we're telling Docker what ports we want to map from our host machine into the container. + +In the previous step, we established that Prose will run internally on port 80. With the above command, we're telling Docker to expose the container's port 80 as port 5000 on our host. + +This means that Prose will be reachable on port 5000 of the host. + +So, if you're running this locally, you'll be able to interact with Prose at `http://localhost:5000`. + +And if you're running it in the cloud on a domain like `https://prose.mydiscussions.com`, you'd likely want it to be listening on port 443 so the user doesn't have to enter a port number as part of the URL. + +### Configure Prose + +As suggested above, you can configure Prose through the use of environment variables. + +You can find a comprehensive list of all environment variables on the Prose [Environment Variables](../env-prose) page. + +In this case, you really only need to set a value for `PROSE_DISCOURSE_HOST`, which will instruct Prose which Discourse instance you'd like it to interact with. + +Additionally, if you'd like to set a different port mapping, you can adjust the `-p` flag of the `docker run` command to something else, such as: + +```bash +-p 8080:80 +``` + +## Install Manually + +This section, whether being done locally or remotely on a cloud provider, will require you to install and configure the necessary dependencies to build and run Prose from scratch. + +### Setup Development Machine + +If you haven't already, setup your machine for Prose development. You can do so by following the guide at [Setup your Development Machine](setup). + +By the time you're done with this step, you should have a local copy of the Lexicon repository on your desired machine. + +### Configure Environment Variables + +The Prose GraphQL API, at a bare minimum, requires you to provide a URL to an accessible Discourse instance in order to run properly. + +Because we're doing this manually, you'll need to specify this in a different way than you would for Docker. + +Later on, once you've built Prose, one way you can specify this is to simply provide it inline as you launch the server. + +```bash +PROSE_DISCOURSE_HOST=https://discourse.mysite.com node lib/index.js +``` + +However, you might find it more ergonomic to leverage the support we've setup for `.env` files. + +The entire Prose codebase lives in the `api/` directory of the repository, so get started by navigating there from the project root. + +``` +$ cd api/ +``` + +Next, you'll need to create a `.env` file. Simply copy the template file, `.env.example` into the `.env` file using the following command. + +``` +$ cp .env.example .env +``` + +After that, as you'd expect, you want to adjust the `.env` file so that it contains the values specific to your project. + +```bash +PROSE_DISCOURSE_HOST= +PROSE_APP_PORT= +``` + +As was covered in the Docker section above, you can find a comprehensive list of all environment variables on the Prose [Environment Variables](../env-prose) page. + +### Launch the Prose GraphQL API + +:::info +At this point, you should already have all the project's dependencies installed. + +If you encounter any errors about missing packages, go back to the guide at [Setup your Development Machine](setup). +::: + +If you'd just like to launch Prose to check it out quickly, you can simply run (from the `api/` directory): + +```bash +$ npm run dev +``` + +This will prepare and spin up Prose in a way that isn't ideal for production. + +If you wish to run the Prose GraphQL API in the background as a process, there are multiple solutions. + +One method is to use **[Tmux](https://github.com/tmux/tmux)**, which will detach the process from the terminal, allowing you to close it and keep Prose running. + +Another method is to use **[PM2](https://pm2.keymetrics.io/)**, which is a sophisticated toolset for running Node processes in production. + +#### Using Tmux + +**Tmux** can be used to detach processes from their controlling terminals, allowing sessions to remain active without being visible. + +To get started, install `tmux` on your machine. + +If you are unsure of how to install tmux, you can follow the instructions on [this page](https://github.com/tmux/tmux#installation). + +Once it's installed, launch it as follows: + +```bash +$ tmux +``` + +Then you can run Prose in the same way as before. + +```bash +$ npm run dev +``` + +If you want to detach from your current session, press `Ctrl + B` then press `d` on your keyboard. The session will remain active in the background. + +And if you wish to re-attach to your last session, run the following command. + +``` +$ tmux a +``` + +If you want to learn more about the tmux command, check out [this cheat sheet](https://tmuxcheatsheet.com/). + +#### Using PM2 + +Another way to run Prose in the background is to use **pm2** (process manager for NodeJS). + +First, as you'd expect, you'll need to install `pm2` on your machine. + +``` +$ npm install -g pm2 +``` + +Once it's installed, you'll also need to use `pm2` to install [Typescript](https://typescriptlang.org/). + +This is because Prose is written in Typescript, and this allows PM2 to run the Typescript files directly for us (as opposed to transpiling them and outputting them as JS first). + +To do this, simply run the following command: + +``` +$ pm2 install typescript +``` + +After that, you can now launch the Prose GraphQL API in the background with: + +``` +$ pm2 start src/index.ts +``` + +To list all running applications, run the following command. + +``` +$ pm2 list +``` + +These are some of the frequently used commands. + +``` +$ pm2 stop # To stop a process +$ pm2 restart # To restart a process +$ pm2 delete # To delete a process +``` + +## Test the GraphQL API + +Now that you've successfully launched Prose, you can actually interact with it in your web browser. + +Because of the libraries that we leveraged in building Prose, it automatically comes with [GraphiQL](https://www.graphql-yoga.com/docs/features/graphiql). + +This is an in-browser GraphQL IDE that makes it easy to explore the documentation and the schema of the GraphQL API. + +In order to access it, you'll need to make note of the host and port number that you configured the API with. + +For example, if you launched Prose from your local machine on port 5000, you'd navigate to [http://localhost:5000](http://localhost:5000). + +Similarly, if you set it up in the cloud, and all you have is an IP address with Prose listening on port 80, you would navigate to something like [http://174.31.92.1](http://174.31.92.1). + +Once the [GraphiQL](https://www.graphql-yoga.com/docs/features/graphiql) interface loads, you can test out some example queries and mutations, including logging into Discourse through Prose. + +### Login + +:::info +If you're accessing a private Discourse site, you'll need to make note of the token that is returned to make other requests. See below. +::: + +``` +mutation Login { + login(email: "user@lexicon.com", password: "user_password") { + ... on LoginOutput { + token + user { + id + name + username + avatarTemplate + } + } + } +} +``` + +As mentioned in the notice, if you're interacting with a private Discourse site, you'll need to provide a token for other GraphQL requests. + +As part of the response for the above mutation, you'll notice a "token" field which contains your authentication token in Base64. + +You use this token in other queries and mutations by opening the HTTP Headers section on the bottom left-hand side of the page. + +This section expects JSON, with which you'll want to add an Authorization header that contains your token. + +```json +{ + "Authorization": "" +} +``` + +Once you have done that, you can make authenticated GraphQL queries and mutations as the user you logged in with. + +### User Profile + +``` + query UserProfile { + userProfile(username: "john_doe") { + user { + ... on UserDetail { + id + avatarTemplate + username + name + websiteName + bioRaw + location + dateOfBirth + email + } + } + } + } +``` + +### Topic Detail + +``` +query TopicDetail { + topicDetail(topicId: 1) { + id + title + views + likeCount + postsCount + liked + categoryId + tags + createdAt + postStream { + posts { + id + topicId + userId + name + username + avatarTemplate + raw + createdAt + } + stream + } + details { + participants { + id + username + avatarTemplate + } + } + } +} +``` + +### Logout + +``` + mutation Logout { + logout (username: "john_doe") + } +``` diff --git a/documentation/versioned_docs/version-2.2.0/tutorial/intro.md b/documentation/versioned_docs/version-2.2.0/tutorial/intro.md new file mode 100644 index 00000000..0098344d --- /dev/null +++ b/documentation/versioned_docs/version-2.2.0/tutorial/intro.md @@ -0,0 +1,47 @@ +--- +title: Overview +slug: /tutorial +--- + +:::info +This tutorial **does not** cover the process of actually launching the app, as well as certain details about deploying to production. For support with those tasks, please refer to the documentation. +::: + +## Welcome to the Lexicon Tutorial + +We're really excited to help you dig in with the Lexicon Stack and learn how to deploy it in a way that benefits you and your users. + +## Target Audience & Prerequisites + +In order to complete this tutorial, you should have familiarity with: + +- The command-line +- Git and Github +- Setting up a Discourse instance +- Setting up servers in general + +In terms of prepararation, you will need: + +- NodeJS installed on your development machine + - Use the latest version of Node that is compatible with the project's version of Expo (i.e. `expo-cli`). +- An editor to edit config files + +#### Have some concerns? + +Interested in Lexicon but lacking in technical abilities? We completely understand. + +Reach out to us at support@kodefox.com to chat about how we can help bring your idea to life. + +## Next Steps + +This tutorial will guide you through the process of getting the entire Lexicon Stack up and running **locally** with your Discourse site. + +At the end of the tutorial, you will be able to interact with your Discourse site in the Lexicon Mobile App on your local device or simulator. + +You will also have an understanding of: + +- How to configure and run the Prose GraphQL API locally or on a server you own +- How to configure and run the Lexicon Mobile app on your device or in a simulator +- The next steps needed to make full use of Lexicon + +Let's get started! diff --git a/documentation/versioned_docs/version-2.2.0/tutorial/publishing.md b/documentation/versioned_docs/version-2.2.0/tutorial/publishing.md new file mode 100644 index 00000000..66005bdd --- /dev/null +++ b/documentation/versioned_docs/version-2.2.0/tutorial/publishing.md @@ -0,0 +1,97 @@ +--- +title: Publish your App +--- + +## EAS Submit + +EAS Submit is a service for uploading and submitting your application binaries to App Store and/or Play Store. +Check [here](https://docs.expo.dev/submit/introduction/) to learn more about EAS Submit. + +### Prerequisites: + +- Registered app in App Store Connect, see the guide [here](../app-store#register-a-new-bundle-id). +- Registered app in Play Store, see the guide [here](../play-store). + +### Configuration + +Before submitting, you are required to specify the credentials to publish your app. + +#### iOS + +For iOS, fill in your account information for `appleId`, `ascAppId`, and `appleTeamId`: + +```json + "base": { + "ios": { + "appleId": "", + "ascAppId": "", + "appleTeamId": "" + }, + ... + }, +``` + +- **appleId**: your apple ID (e.g., `john@gmail.com`). +- **ascAppId**: your App Store Connect app ID. Find your ascAppID by following [this guide](https://github.com/expo/fyi/blob/main/asc-app-id.md) (e.g., `1234567890`). +- **appleTeamId**: You can check your apple team ID [here](https://developer.apple.com/account/) (e.g., `12LE34XI45`). + +#### Android + +For Android, you will need to add a `.json` key file to authenticate with the Google Play Store. Please follow [this guide](https://github.com/expo/fyi/blob/main/creating-google-service-account.md) to generate one. Then, copy the JSON file to your `lexicon/frontend` directory, and rename the file as `playstore_secret.json`. + +The JSON file looks like this: + +```json +{ + "type": "service_account", + "project_id": "", + "private_key_id": "", + "private_key": "-----BEGIN PRIVATE KEY----------END PRIVATE KEY-----\n", + "client_email": "", + "client_id": "", + "auth_uri": "", + "token_uri": "", + "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", + "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/lexicon%40api.iam.gserviceaccount.com" +} +``` + +Now that the configuration is done, you can start submitting your app. + +### Submitting + +Use this command to submit the build: + +```bash +eas submit --platform ios --profile +``` + +Then you will see the EAS CLI prompt asking which app you would like to submit. + +There are 4 possible options: + +- Selecting a build from EAS +- Providing the URL of an app archive +- Providing the local path to an app binary file +- Providing the build ID of an existing build on EAS + +If you have built your app using EAS Build or have been following the tutorial from [Build your App](building), then please choose the first option, and select the version you want. + +### Submit Profiles + +By default, `eas.json` has been configured with two submit profiles, which are **staging** and **production**. + +The configuration is mostly the same, the only difference lies in the Android track options. + +- Staging infers the track as `internal`. This means submitting with the staging profile will submit the build for internal testing in the Play Store. +- Production infers the track as `production`, which will submit the build for Public Release in the Play Store. + +With iOS, on the other hand, both profiles will be submitted to TestFlight before you can release them publicly. + +You can reference the Expo documentation to learn more about [Android-specific](https://docs.expo.dev/submit/eas-json/#android-specific-options) and [iOS-specific](https://docs.expo.dev/submit/eas-json/#ios-specific-options) options. + +## Congratulations! + +Your app is now available for users to download from both the Play Store and the App Store! 🥳 + +To learn more about how to update your published app in the case of a bug, as well as OTA updates, check out the [next and final section](updating) of the tutorial. diff --git a/documentation/versioned_docs/version-2.2.0/tutorial/setup-cloud-server.md b/documentation/versioned_docs/version-2.2.0/tutorial/setup-cloud-server.md new file mode 100644 index 00000000..b69659a4 --- /dev/null +++ b/documentation/versioned_docs/version-2.2.0/tutorial/setup-cloud-server.md @@ -0,0 +1,27 @@ +--- +title: Setup a Cloud Server (Optional) +--- + +import useBaseUrl from '@docusaurus/useBaseUrl'; + +:::info +This is an optional section for users that don't feel as confident spinning up a new server with a cloud provider. + +If you are already adept at this, you can skip to the next section. +::: + +## DigitalOcean Guide + +### How To Set Up an Ubuntu 20.04 Server on a DigitalOcean Droplet + +For our users that aren't as familiar with setting up servers in the cloud, we wanted to provide you with a solid resource to learn more about it and accomplish something in the process. + +DigitalOcean has already provided an excellent guide to walk you through this, so we're going to link you over to them. + +In this guide, you will create an Ubuntu server through DigitalOcean’s administrative panel and configure it to work with your SSH keys. + +Once you have a solid understanding of how to setup servers in the cloud, you'll be much more capable of deploying the Lexicon Stack for your users. + +You can dig in on the article below. + +[Read: How To Set Up an Ubuntu 20.04 Server on a DigitalOcean Droplet](https://www.digitalocean.com/community/tutorials/how-to-set-up-an-ubuntu-20-04-server-on-a-digitalocean-droplet) diff --git a/documentation/versioned_docs/version-2.2.0/tutorial/setup-discourse.md b/documentation/versioned_docs/version-2.2.0/tutorial/setup-discourse.md new file mode 100644 index 00000000..9b5bf97a --- /dev/null +++ b/documentation/versioned_docs/version-2.2.0/tutorial/setup-discourse.md @@ -0,0 +1,306 @@ +--- +title: Prepare a Discourse Instance +--- + +Before you can properly setup Lexicon, you'll need to have a running **[Discourse](https://www.discourse.org/)** instance for Lexicon to interact with. + +For this step, you actually have a few options: + +#### Option 1: Setup a Local Discourse Instance + +The first option is to [setup a development instance](#setup-discourse-locally) of Discourse locally on your development machine. This takes a bit of time and can get a bit technical. + +#### Option 2: Buy a Discourse Instance or Use your Existing One + +The second option is to pay to [setup a Discourse instance in the cloud](#setup-discourse-in-the-cloud) as a live, reachable production verison. This is much simpler, but has the obvious tradeoff of costing money. + +And perhaps it goes without saying, but if you already have a Discourse site, feel free to just use that. + +#### Option 3: Use a Public Discourse Site + +The third option is to use an existing Discourse site just to test things out. + +As you'll see later on, Lexicon allows you to configure which Discourse site it is pointing at. As such, you can instruct it to point at at a publically accessible Discourse site that you don't personally own. + +There are countless examples of active Discourse communities out there. Here are a few examples to choose from: + +##### Discourse Meta + +[https://meta.discourse.org/](https://meta.discourse.org/) + +##### Expo + +[https://forums.expo.dev/](https://forums.expo.dev/) + +##### The Rust Programming Language + +[https://users.rust-lang.org/](https://users.rust-lang.org/) + +##### FreeCodeCamp Forums + +[https://forum.freecodecamp.org/](https://forum.freecodecamp.org/) + +## Setup Discourse Locally + +:::note +This section can take a long time. Depending on the specs of your machine, it could take between 10 - 30 minutes to complete. +::: + +This section of the tutorial is based on the following post on Discourse: [Beginners Guide to Install Discourse for Development using Docker](https://meta.discourse.org/t/beginners-guide-to-install-discourse-for-development-using-docker/102009). + +If you run into any issues, feel free to reference the original post and subsequent discussion. + +### Install Docker + +**[Docker](https://www.docker.com/)** is a containerization framework that makes it easy to build, manage, and deploy your application stack in a way that is safer, more reliable, and repeatable across multiple platforms. + +When developing, building, and testing applications locally, it is an invaluable tool that greatly simplifies the entire process. + +The main way that Docker helps us in this tutorial is that it won't require any modifications to our machine's environment other than installing Docker itself. + +This is as-opposed to needing to install all of Discourse's dependencies on your physical machine, in a way that may take a lot of effort to undo later. + +If you are unsure of how to install Docker, you can follow the instructions on their [website](https://www.docker.com/get-started). + +### Clone Discourse + +Once Docker is up and running, we can get started with setting up Discourse locally. + +The first step is to clone the Discourse repository to your local machine and `cd` into it. + +``` +git clone https://github.com/discourse/discourse.git +cd discourse +``` + +Note the repository is on the larger side (nearly 400mb), so this step may take a while depending on your connection. + +### Pull, Build, and Start the Discourse Dev Container + +:::caution +Make sure that the **host ports** listed below are not already in use on your device. +::: + +Discourse already contains a script to help spin up its entire infrastructure using Docker. + +During this process, the script will do the following: + +- Pull down the necessary "dev" Docker image to bootstrap Discourse +- Build the aforementioned image +- Run the image as a container with multiple ports mapped from your host into the container + - 127.0.0.1:**1080**->1080/tcp + - 127.0.0.1:**3000**->3000/tcp + - 127.0.0.1:**4200**->4200/tcp + - 127.0.0.1:**9292**->9292/tcp + - 127.0.0.1:**9405**->9405/tcp +- Prompt you for an admin email address and password + +To get started, simply run the following command: + +``` +$ d/boot_dev --init +``` + +Note that all of the Docker images add up to about 1GB of disk space usage on your device. + +The command will pause when it needs information from you. As shown below, it will prompt you for an administrator email address and password. + +```bash +# Output omitted +== 20200804144550 AddTitleToPolls: migrating ================================== +-- add_column(:polls, :title, :string) + -> 0.0014s +== 20200804144550 AddTitleToPolls: migrated (0.0021s) ========================= + +Creating admin user... +Email: me@me.com +Password: +Repeat password: + +Ensuring account is active! + +Account created successfully with username me +``` + +Next, it will ask you if you want to make this account an admin account. You do. + +```bash +Do you want to grant Admin privileges to this account? (Y/n) y + +Your account now has Admin privileges! +``` + +Please be aware, as suggested above, that the ports mentioned above are not currently in use by other processes. + +### If something unexpected happened + +It's possible that something strange may have happened at this step. + +Perhaps there was a weird error message, or the process just never displayed the output shown above. + +What we'd recommend doing is the following: + +#### Check if a Docker container named `discourse_dev` is running + +```bash +$ docker ps | grep discourse_dev +CONTAINER ID IMAGE ... NAMES +dc72a4ead10f discourse/discourse_dev:release ... discourse_dev +``` + +If it is, stop and remove the container. + +```bash +$ docker stop discourse_dev +discourse_dev +$ docker rm discourse_dev +discourse_dev +``` + +#### Exit or Kill the Existing Process + +If the existing process (`d/boot_dev --init`) is still occupying your terminal session, attempt to exit it via `Ctrl + C`. + +If the process is not responding to `Ctrl + C` after some time, locate its PID and use `kill -9` to kill it + +```bash +$ ps aux | grep rails +user 81254 0.0 0.1 discourse_dev bin/rails s + +$ kill -9 81254 +``` + +#### Restart Docker or your Machine + +Using the command or interface appropriate for your machine, you should restart all of Docker. + +On Docker for Mac, this is as simple as going into the tray icon and clicking Restart. + +#### Try running the command again + +Sometimes things just go a little haywire with this setup. Try running the command again to see if it works better this time. + +#### If you're absolutely stuck, reach out. + +Don't hestitate to contact us if you're just stuck with this one. + +### Optional: Run the Next Two Commands in the Background + +You can read on to get an understanding of what the two commands are, but it's worth mentioning that you want them to run simultaneously. + +You can do this by _backgrounding_ both processes. + +This means that they won't occupy your current session, requiring you to quit them in order to enter other commands. + +When you run this command, it will show you the process IDs (PIDs) of the processes that were backgrounded. + +To bring them back into the foreground, you can run the `fg` command, and then use `Ctrl + C` or a similar signal to stop them. + +```bash +d/rails s & d/ember-cli & +[2] 59786 +[3] 59787 + +fg +``` + +Just **note** that you won't see the output of the commands, and so you may need to be patient for several minutes until Discourse is reachable at its local address. + +Alternatively, you can use the PIDs to kill the processes outright in another session: + +```bash +kill -9 59786 59787 +``` + +### Start the Rails Server within the Container + +If you hadn't already noticed, Discourse is built in [Ruby](https://www.ruby-lang.org/en/) using the very popular web framework, [Ruby on Rails](https://rubyonrails.org/). + +By running the command below, you will be starting the Rails server, which will take some time, and will produce a tremendous amount of output. + +In particular, you'll see the database being initialized as the dev container bootstraps the Discourse server. + +To get started, simply run the following command. + +``` +d/rails s +``` + +#### If you later can't quit the process + +**Note** that this command can sometimes hang when you're trying to kill it with `Ctrl + C`. + +If that happens, it's recommended that you first stop the Docker container: + +```bash +docker stop docker_dev +``` + +Then, bring the process to the foreground with `fg` if necessary. + +Last, either exit your session if possible - such as by closing the Terminal - or find out the PID of the Rails process and kill it directly. + +```bash +$ ps aux | grep rails +user 81254 0.0 0.1 discourse_dev bin/rails s + +$ kill -9 81254 +``` + +### Run the Ember CLI + +The above section mentioned Ruby on Rails, which handles the backend aspects of the Discourse application. + +However, the Discourse frontend is build in [EmberJS](https://emberjs.com/), which is a batteries-included frontend web framework used by multiple major companies. + +Run the command below to instruct the Ember CLI to start the Discourse frontend. + +``` +d/ember-cli +``` + +Once you have done this, you'll be able to access Disourse at [http://localhost:4200](http://localhost:4200). + +Please note that the output of this command can be a bit confusing. And at times, it can seem like nothing is happening. + +You may see several progress indicators, as well as blank output, for several minutes before the server is ready. + +The output you're looking for will resemble the following: + +```bash +Build successful (72475ms) – Serving on http://localhost:4200/ + +Slowest Nodes (totalTime >= 5%) | Total (avg) +----------------------------------------------------------------------+------------------ +Babel: discourse (2) | 31501ms (15750 ms) +ember-auto-import-analyzer (11) | 10418ms (947 ms) +Bundler (1) | 6119ms +Babel: @ember/test-helpers (2) | 5075ms (2537 ms) +broccoli-persistent-filter:TemplateCompiler (3) | 4596ms (1532 ms) +``` + +## Setup Discourse in the Cloud + +There are several guides with instructions on how to setup Discourse in the Cloud. + +Rather than writing another one, we have found our favorite one and would like to send you over to them to give you a proper walkthrough of the process. + +### Guide by SSDNodes + +The guide is provided by the [SSDNodes](https://www.ssdnodes.com/?e=blog&q=more-about-ssdnodes) Blog, [Serverwise](https://blog.ssdnodes.com/blog/). + +If you aren't familiar, [SSDNodes](https://www.ssdnodes.com) is an excellent, cost-effective VPS hosting provider. + +While we are most familiar with Digital Ocean, we'd strongly encourage you to check them out as an alternative for hosting Discourse. + +The post, titled [How To Install Discourse On Ubuntu](https://blog.ssdnodes.com/blog/install-discourse/), is written by [Joel Hans](https://blog.ssdnodes.com/blog/author/joel/). + +Joel has written an excellent guide. He'll take you through the entire process, including making update to your Discourse instance. + +If you find yourself stuck, or have any questions, feel free to reach out to us. + +## Use a Public Discourse Site + +If you've chosen this option, there's not much to do other than to note the URL of the Discourse site you'll be using. + +Once you have that written down somewhere, you're ready for the next section. diff --git a/documentation/versioned_docs/version-2.2.0/tutorial/setup-mobile.md b/documentation/versioned_docs/version-2.2.0/tutorial/setup-mobile.md new file mode 100644 index 00000000..62ab943a --- /dev/null +++ b/documentation/versioned_docs/version-2.2.0/tutorial/setup-mobile.md @@ -0,0 +1,120 @@ +--- +title: Configure & Launch the Mobile App +--- + +import useBaseUrl from '@docusaurus/useBaseUrl'; + +After following the **[Setup the Prose GraphQL API](install-prose)** section, your GraphQL API should now be connected to your Discourse site. + +Next, we'll guide you through the process of connecting the Lexicon Mobile App to your Discourse site via Prose. + +### Mobile App Configuration + +:::note +In the original release of Lexicon, the **Prose URL** was specified in `frontend/.env`. However, as part of migrating to Expo's EAS feature, we centralized the configuration into `frontend/Config.ts` to save you the trouble of needing to maintain it in more than one place, as suggested in the [Expo documentation](https://docs.expo.dev/build-reference/variables/#can-i-share-environment-variables-defined-in-easjson-with-expo-start-and-eas-update) +::: + +Before launching your local version of the Lexicon Mobile App, you'll need to configure it with at least one piece of information. + +The Lexicon Mobile app relies exclusively on a running instance of the Prose GraphQL API in order to retrieve data from your Discourse instance. + +Therefore, you'll need to instruct it on how to locate your running Prose server. + +In development, it is common to have it running locally. However, if you have already deployed Prose +somewhere, feel free to use that. + +#### Configuring `proseUrl` via `config` + +:::caution + +##### `proseUrl` requirements + +It is worth noting that `proseUrl` **must** start with either `http://` or `https://`. + +If it does not, the Mobile App will throw an error when launching. +::: + +`Config.ts` contains the `config` object, which allows you to specify the Prose URL for each scenario encountered when developing and building the Mobile App. + +The specific configuration value which enables this is `proseUrl`, and it is contained within each scenario expressed by the `config` object. + +```ts +const config = { + localDevelopment: { + proseUrl: 'http://localhost:8929', + }, + buildChannels: { + preview: { + proseUrl: 'https://preview.myserver.com:8080/subpath', + }, + production: { + proseUrl: 'https://myserver.com/api/prose', + }, + }, +}; +``` + +As mentioned earlier—above, the `config` object allows us to express configuration values for multiple scenarios, which are: + +- `localDevelopment`: when developing against the app locally. This configuration is also used as a fallback for an unknown build channel. +- `buildChannels`: used to define configuration by build channel when building the app with the EAS CLI. + +`buildChannels` makes use of Expo's build channels (typically `preview` and `production`) as its keys. + +Each key within `buildChannels` maps to a specific Prose URL, which will be used for the build version based on which channel you build for. + +From the example above, when we create a `preview` build, the app will be built and configured to contact a Prose server located at `https://preview.myserver.com:8080/subpath`. + +The example above expresses a setup in which each build has its own deployed Prose server. However, it is also common to use one server for all scenarios, including development. + +```ts +const config = { + localDevelopment: { + proseUrl: 'https://myserver.com/api/prose', + }, + buildChannels: { + preview: { + proseUrl: 'https://myserver.com/api/prose', + }, + production: { + proseUrl: 'https://myserver.com/api/prose', + }, + }, +}; +``` + +##### Port Number + +Bear in mind that if your Prose server is not running on port 80 or 443, you also need to specify the **port number** via `proseUrl`. + +For example, if you've started a Prose server **locally** on port `8929` and try to run it using `expo start`, your `Config.ts` file would contain `http://myserver.com:8929/api/prose` under `localDevelopment.proseUrl`. + +### Launch the Mobile App + +Once you have configured everything, you'll want to launch the Mobile App to test that it is speaking to the right Prose server. + +To do this, you can simply run the following from the project root: + +```bash +npm run --prefix frontend start +``` + +The Expo development server should launch, and you can follow the instructions to run the app in a simulator or on your actual device. + +#### Troubleshooting + +If the app throws an error upon loading, you should double-check the configuration values you specified, according to the message you've received. + +If the app loads, but you're unable to actually connect, you should verify the following: + +- Your Prose Server is up and running at the location you provided to the Lexicon Mobile App +- Your Prose Server is configured to point at an accessible Discourse instance +- Your Discourse instance is up and running correctly + +## Nice Work! + +At this point, you've already accomplished a lot. + +The Discourse server you started off with is now accessible in a new way from a sleek native mobile app, and you're free to customize it to your heart's content. + +In the next part of the tutorial, we'll briefly get into that very topic: customizing the Mobile App to [white label](white-label) it for your brand. diff --git a/documentation/versioned_docs/version-2.2.0/tutorial/setup.md b/documentation/versioned_docs/version-2.2.0/tutorial/setup.md new file mode 100644 index 00000000..20505940 --- /dev/null +++ b/documentation/versioned_docs/version-2.2.0/tutorial/setup.md @@ -0,0 +1,98 @@ +--- +title: Setup your Development Machine +--- + +## Install NodeJS + +If you haven't already, install NodeJS on your machine. + +The tooling needed to setup Lexicon relies heavily on Node and npm. + +If you are unsure of how to install NodeJS, you can follow the instructions on the [NodeJS Website](https://nodejs.org/en/download/). + +#### Supported Node Versions + +It is recommended that you perform this tutorial using the latest version of Node that is compatible with the the project's version of Expo. + +You can always confirm this by viewing the dependencies in [frontend/package.json](https://github.com/lexiconhq/lexicon/blob/master/frontend/package.json). + +If your setup doesn't allow you to easily change your current Node version, we would recommend making use of [`nvm`](https://github.com/nvm-sh/nvm) to quickly switch between Node versions. + +### Install yarn, if you prefer + +Lexicon doesn't leverage any special features of [Yarn](https://yarnpkg.com/) - the alternative package manager for Node. If you prefer it, it will work the same as running `npm install`. + +For the purposes of this tutorial, we will demonstrate all commands using `npm`. + +### Clone the Lexicon Repository + +In a desirable location on your development machine, clone the Lexicon repository and `cd` into it. + +```sh +git clone git@github.com:lexiconhq/lexicon.git +cd lexicon +``` + +### Install Dependencies + +Next, install Lexicon's dependencies: + +```sh +npm install +``` + +This will install dependencies for both the Mobile App and the backend GraphQL API, Prose. + +### Install the Expo CLI + +[Expo](https://expo.io/) is the phenomenal toolchain that Lexicon uses to develop and build the Mobile App. + +We will later use the Expo CLI to launch the Mobile App - either on your device or in a simulator. + +You can install the Expo CLI with the following command: + +```sh +npm install --global expo-cli +``` + +Further information is available in the [Expo docs](https://docs.expo.io/). + +Then, verify that Expo is available in your `PATH` with the following: + +```sh +$ expo --version + +``` + +### Install the EAS CLI + +[Expo Application Services (EAS)](https://expo.dev/eas/) is an integrated set of cloud services for Expo and React Native apps. + +We will use the EAS CLI to build and publish the Mobile App. + +You can install the EAS CLI with the following command: + +```sh +npm install --global eas-cli +``` + +Further information is available in the [Expo docs](https://docs.expo.dev/eas/). + +Then, verify that EAS is available in your `PATH` with the following: + +```sh +$ eas --version +eas-cli/ +``` + +### Ready to Go! + +That's all we need for this step. + +Next, there is an optional guide to help you if you're not too familiar with setting up a server on a cloud provider. + +You're free to skip this if you're already adept at this process. + +After that, we'll look into how we can prepare Discourse to connect with the Lexicon Mobile App. + +If you don't already have a Discourse server setup, we'll get into that as well. diff --git a/documentation/versioned_docs/version-2.2.0/tutorial/updating.md b/documentation/versioned_docs/version-2.2.0/tutorial/updating.md new file mode 100644 index 00000000..46fcfdd1 --- /dev/null +++ b/documentation/versioned_docs/version-2.2.0/tutorial/updating.md @@ -0,0 +1,68 @@ +--- +title: Update your App +--- + +## EAS Update + +EAS Update is the successor to `expo publish`. This service helps to update projects using the `expo-updates` library. + +In particular, it enables you to push quick fixes to your users in between full-fledged app store submissions. + +With EAS Update, there is no need to recompile the app with its non-native parts, such as TypeScript code, styling, or image assets. [Click here](https://docs.expo.dev/eas-update/introduction/) to learn more about EAS Update. +:::note +You are required to build the app with [EAS Build](building) before using the EAS Update. +::: + +### Configuration + +Let's get started by configuring EAS update. Feel free to check out the [complete guide](https://docs.expo.dev/build-reference/build-configuration/) from Expo for further details. + +```bash +eas update:configure +``` + +Running this command will add `expo.updates.url` and `runtimeVersion.policy` in `app.json`. + +:::caution + +As mentioned in the [Expo documentation](https://docs.expo.dev/build/updates/#previewing-updates-in-development-builds), you can no longer launch your app in Expo Go (using `expo start`) after adding the `runtimeVersion` field in `app.json`. It is recommended to use `expo-dev-client` instead to create a development build. + +```bash +eas -p all -e development +``` + +or if you still wish to use Expo Go, please remove `runtimeVersion` field from `app.json` before running `expo start`. +::: + +### Updating + +After making the necessary changes, you can push updates using this command: + +```bash +eas update –-branch –-message “” +``` + +The branch name here is the same as the build profile name when building the app. +For example, if you had previously built the app with this command: + +```bash +eas build –p all –e preview +``` + +Then you can later update it using: + +```bash +eas update –-branch preview –-message “Fixing typos” +``` + +Once the update is complete, force close and reopen the installed app twice to view the update. + +## All Done! 🙌 + +That's it for the tutorial. Great work. + +We hope that this has served as an informative guide to help familiarize you with Lexicon and how you can make use of it. + +If you haven't already, check out the [Lexicon Documentation](../) to get a deeper understanding of the project and how it all works. + +If you have any questions, comments, feedback, or want to contribute, please reach out to us on Github! diff --git a/documentation/versioned_docs/version-2.2.0/tutorial/white-label.md b/documentation/versioned_docs/version-2.2.0/tutorial/white-label.md new file mode 100644 index 00000000..f0192667 --- /dev/null +++ b/documentation/versioned_docs/version-2.2.0/tutorial/white-label.md @@ -0,0 +1,82 @@ +--- +title: White Label your App +--- + +## Customize the Splash Screen and App Icon + +In order to customize the app for your own brand, you will likely want to provide your own assets for the **Splash Screen** and the **App Icon**. + +The **Splash Screen** - sometimes also referred to as the Launch Screen - is what appears while the app is launching. Some apps also display this to help conceal private information when the app is put into background mode. + +The **App Icon** is what is used to represent the app on the user's device, such as on the home screen and when listing it in the device's settings. + +Both of these assets often contain your logo in one form or another. For example, the App Icon for the Gmail app is the multi-colored outline of an envelope. Then, when launching the Gmail app, you will notice that the Splash Screen includes a larger version of the App Icon. + +### Customizing the Splash Screen + +:::info +Expo does not currently support dark mode for splash screens. +::: + +The assets used for the splash screen in the Mobile App are located at `frontend/assets/images/splash.png` and `frontend/assets/images/splashDark.png`. + +Above, we mention splash screen assets for both Dark Mode and Light Mode. + +However, unfortunately at this time, Expo does not support Dark Mode for Splash Screens. We have only included both so that they're ready when Expo finally does support this. + +In the meantime, you're free to adjust `splash.png` to influce what asset appears. + +In order to change it, you can simply replace the existing file with your own `splash.png`. + +To find out more about the Splash Screen image size and other details, please see the [Expo Splash Screen Guide](https://docs.expo.io/guides/splash-screens/). + +#### Futher Configuration + +To resize the Splash Screen image and change its background color, first open `frontend/app.json` and locate the `"splash"` field within it. + +As illustrated by the excerpt below, there are multiple fields that can be used to further adjust the Splash Screen: + +```json +"splash": { + "image": "./assets/splash.png", + "resizeMode": "contain", + "backgroundColor": "#FFFFFF" +}, +``` + +**image** + +The `image` field is fairly self-explanatory - it allows you to adjust what path will be used to locate the Splash Screen image. + +**resizeMode** + +The `resizeMode` field allows you to manage how the Splash Screen image will be resized to maintain its aspect ratio: + +- `contain` - Resize the image to make sure the whole image is visible. This is the default setting. +- `cover` - Resize the image to cover the entire container (in this case the whole screen) by either stretching or cropping the image as needed. + +Further details of how `contain` and `cover` behave are covered in the previously mentioned [Expo Splash Screen guide](https://docs.expo.io/guides/splash-screens/). For an even more detailed explanation, you can read [this post](http://blog.vjeux.com/2013/image/css-container-and-cover.html). + +**backgroundColor** + +The `backgroundColor` field enables you to specify the color of the background behind the Splash Screen image. Removing this value will result in usage of the default value, which is a white background color. + +### Customizing the App Icon + +Customizing the App Icon in Lexicon is nearly the same process as customizing the Splash Screen. + +The image asset for the Mobile App's icon is located at `frontend/assets/icon.png`. To customize it, simply overwrite that file with your own `icon.png`. + +## Further Customization + +We get into more detail about how to white label your app in the [White Labeling](../white-labeling) section of the documentation. + +In particular, this includes customizing and extending the theme's color palette, icons, and even fonts. + +Should you wish to customize anything not covered in that section, get in touch with us, and we'll see how we can help you make it a reality. + +## Awesome Work + +Your app looks cool now 😎. However, it's only accessible to you. + +Next, we'll cover how you can actually [build your app](building), so you can share it with the world. diff --git a/documentation/versioned_docs/version-2.2.0/white-labeling.md b/documentation/versioned_docs/version-2.2.0/white-labeling.md new file mode 100644 index 00000000..be413831 --- /dev/null +++ b/documentation/versioned_docs/version-2.2.0/white-labeling.md @@ -0,0 +1,13 @@ +--- +title: Overview +--- + +The Lexicon Mobile App allows you to customize its appearance through a process known as **White Labeling**. + +If you're unfamiliar with this term, it's essentially the process of branding an existing application specifically for your users. + +White Labeling allows you to configure the app with your own logo, app icon, color theme, fonts, and so on. + +The idea is that your users won't know that the Lexicon team built this app. Its appearance will be completely customized to your brand. + +To learn more about White Labeling the Lexicon Mobile App, continue to the next section. diff --git a/documentation/versioned_sidebars/version-2.1.0-sidebars.json b/documentation/versioned_sidebars/version-2.1.0-sidebars.json new file mode 100644 index 00000000..36d1e847 --- /dev/null +++ b/documentation/versioned_sidebars/version-2.1.0-sidebars.json @@ -0,0 +1,57 @@ +{ + "docs": { + "Lexicon": [ + "intro", + "rationale", + "technologies", + "concepts", + "discourse-features", + "supported-devices", + "contributing", + "commercial-support" + ], + "Getting Started": ["quick-start", "setup", "customize"], + "Configuring the Mobile App": ["env-mobile"], + "White Labeling": ["white-labeling", "assets", "theming"], + "Deploying Prose": ["deployment", "env-prose", "dedicated"], + "Configuring Discourse": ["optimal"], + "Discourse Plugin": [ + "discourse-plugin", + "discourse-plugin-installation", + "discourse-plugin-enable", + { + "Push Notifications": [ + "push-notifications/introduction", + "push-notifications/plugin-interaction", + "push-notifications/setup/enable-push-notifications", + "push-notifications/setup/verify-push-notifications" + ], + "Email Deep Linking": [ + "email-deep-linking/intro", + "email-deep-linking/setup/enable-email-deep-linking", + "email-deep-linking/setup/verify-email-deep-linking" + ] + } + ], + "Publishing your App": [ + "app-store", + "play-store", + "lexicon-updates", + "troubleshooting-build" + ] + }, + "tutorial": { + "Tutorial": [ + "tutorial/intro", + "tutorial/setup", + "tutorial/setup-cloud-server", + "tutorial/setup-discourse", + "tutorial/install-prose", + "tutorial/setup-mobile", + "tutorial/white-label", + "tutorial/building", + "tutorial/publishing", + "tutorial/updating" + ] + } +} diff --git a/documentation/versioned_sidebars/version-2.2.0-sidebars.json b/documentation/versioned_sidebars/version-2.2.0-sidebars.json new file mode 100644 index 00000000..21d9e1ae --- /dev/null +++ b/documentation/versioned_sidebars/version-2.2.0-sidebars.json @@ -0,0 +1,88 @@ +{ + "docs": { + "Lexicon": [ + "intro", + "rationale", + "technologies", + "concepts", + "discourse-features", + "supported-devices", + "contributing", + "commercial-support" + ], + "Getting Started": [ + "quick-start", + "setup", + "customize" + ], + "Configuring the Mobile App": [ + "env-mobile" + ], + "White Labeling": [ + "white-labeling", + "assets", + "theming" + ], + "Deploying Prose": [ + "deployment", + "env-prose", + "dedicated" + ], + "Configuring Discourse": [ + "optimal" + ], + "Discourse Plugin": [ + "discourse-plugin", + "discourse-plugin-installation", + "discourse-plugin-enable", + { + "Push Notifications": [ + "push-notifications/introduction", + "push-notifications/plugin-interaction", + "push-notifications/setup/enable-push-notifications", + "push-notifications/setup/verify-push-notifications" + ], + "Email Deep Linking": [ + "email-deep-linking/intro", + "email-deep-linking/setup/enable-email-deep-linking", + "email-deep-linking/setup/verify-email-deep-linking" + ], + "Login With Link": [ + "login-with-link/intro", + "login-with-link/setup/enable-login-with-link", + "login-with-link/setup/verify-login-with-link" + ], + "Activation Account With Link": [ + "activation-with-link/intro", + "activation-with-link/setup/enable-activate-with-link", + "activation-with-link/setup/verify-activate-with-link" + ], + "Login with Apple": [ + "login-with-apple/intro", + "login-with-apple/setup/enable-login-with-apple", + "login-with-apple/setup/verify-login-with-apple" + ] + } + ], + "Publishing your App": [ + "app-store", + "play-store", + "lexicon-updates", + "troubleshooting-build" + ] + }, + "tutorial": { + "Tutorial": [ + "tutorial/intro", + "tutorial/setup", + "tutorial/setup-cloud-server", + "tutorial/setup-discourse", + "tutorial/install-prose", + "tutorial/setup-mobile", + "tutorial/white-label", + "tutorial/building", + "tutorial/publishing", + "tutorial/updating" + ] + } +} diff --git a/documentation/versions.json b/documentation/versions.json index 04c9f68a..0876847e 100644 --- a/documentation/versions.json +++ b/documentation/versions.json @@ -1 +1,6 @@ -["2.0.0", "1.0.0"] +[ + "2.2.0", + "2.1.0", + "2.0.0", + "1.0.0" +] diff --git a/documentation/yarn.lock b/documentation/yarn.lock index b20f2b1d..a2fa13a1 100644 --- a/documentation/yarn.lock +++ b/documentation/yarn.lock @@ -1732,7 +1732,7 @@ "@docusaurus/theme-search-algolia" "2.4.3" "@docusaurus/types" "2.4.3" -"@docusaurus/react-loadable@5.5.2", "react-loadable@npm:@docusaurus/react-loadable@5.5.2": +"@docusaurus/react-loadable@5.5.2": version "5.5.2" resolved "https://registry.yarnpkg.com/@docusaurus/react-loadable/-/react-loadable-5.5.2.tgz#81aae0db81ecafbdaee3651f12804580868fa6ce" integrity sha512-A3dYjdBGuy0IGT+wyLIGIKLRE+sAk1iNk0f1HjNDysO7u8lhL4N3VEm+FAubmJbAztn94F7MxBTPmnixbiyFdQ== @@ -2908,21 +2908,21 @@ binary-extensions@^2.0.0: resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== -body-parser@1.20.0: - version "1.20.0" - resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.0.tgz#3de69bd89011c11573d7bfee6a64f11b6bd27cc5" - integrity sha512-DfJ+q6EPcGKZD1QWUjSpqp+Q7bDQTsQIF4zfUAtZ6qk+H/3/QRhg9CEp39ss+/T2vw0+HaidC0ecJj/DRLIaKg== +body-parser@1.20.2: + version "1.20.2" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.2.tgz#6feb0e21c4724d06de7ff38da36dad4f57a747fd" + integrity sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA== dependencies: bytes "3.1.2" - content-type "~1.0.4" + content-type "~1.0.5" debug "2.6.9" depd "2.0.0" destroy "1.2.0" http-errors "2.0.0" iconv-lite "0.4.24" on-finished "2.4.1" - qs "6.10.3" - raw-body "2.5.1" + qs "6.11.0" + raw-body "2.5.2" type-is "~1.6.18" unpipe "1.0.0" @@ -2978,11 +2978,11 @@ brace-expansion@^1.1.7: concat-map "0.0.1" braces@^3.0.2, braces@~3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" - integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== + version "3.0.3" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" + integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== dependencies: - fill-range "^7.0.1" + fill-range "^7.1.1" browserslist@^4.0.0, browserslist@^4.14.5, browserslist@^4.16.6, browserslist@^4.18.1, browserslist@^4.20.2, browserslist@^4.20.3, browserslist@^4.21.2: version "4.21.3" @@ -3350,6 +3350,11 @@ content-type@~1.0.4: resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA== +content-type@~1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918" + integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA== + convert-source-map@^1.7.0: version "1.8.0" resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.8.0.tgz#f3373c32d21b4d780dd8004514684fb791ca4369" @@ -3362,10 +3367,10 @@ cookie-signature@1.0.6: resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ== -cookie@0.5.0: - version "0.5.0" - resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.5.0.tgz#d1f5d71adec6558c58f389987c366aa47e994f8b" - integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw== +cookie@0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.6.0.tgz#2798b04b071b0ecbff0dbb62a505a8efa4e19051" + integrity sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw== copy-text-to-clipboard@^3.0.1: version "3.0.1" @@ -4014,16 +4019,16 @@ execa@^5.0.0: strip-final-newline "^2.0.0" express@^4.17.3: - version "4.18.1" - resolved "https://registry.yarnpkg.com/express/-/express-4.18.1.tgz#7797de8b9c72c857b9cd0e14a5eea80666267caf" - integrity sha512-zZBcOX9TfehHQhtupq57OF8lFZ3UZi08Y97dwFCkD8p9d/d2Y3M+ykKcwaMDEL+4qyUolgBDX6AblpR3fL212Q== + version "4.19.2" + resolved "https://registry.yarnpkg.com/express/-/express-4.19.2.tgz#e25437827a3aa7f2a827bc8171bbbb664a356465" + integrity sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q== dependencies: accepts "~1.3.8" array-flatten "1.1.1" - body-parser "1.20.0" + body-parser "1.20.2" content-disposition "0.5.4" content-type "~1.0.4" - cookie "0.5.0" + cookie "0.6.0" cookie-signature "1.0.6" debug "2.6.9" depd "2.0.0" @@ -4039,7 +4044,7 @@ express@^4.17.3: parseurl "~1.3.3" path-to-regexp "0.1.7" proxy-addr "~2.0.7" - qs "6.10.3" + qs "6.11.0" range-parser "~1.2.1" safe-buffer "5.2.1" send "0.18.0" @@ -4149,10 +4154,10 @@ filesize@^8.0.6: resolved "https://registry.yarnpkg.com/filesize/-/filesize-8.0.7.tgz#695e70d80f4e47012c132d57a059e80c6b580bd8" integrity sha512-pjmC+bkIF8XI7fWaH8KxHcZL3DPybs1roSKP4rKDvy20tAWwIObE4+JIseG2byfGKhud5ZnM4YSGKBz7Sh0ndQ== -fill-range@^7.0.1: - version "7.0.1" - resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" - integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== +fill-range@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292" + integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== dependencies: to-regex-range "^5.0.1" @@ -4210,9 +4215,9 @@ flux@^4.0.1: fbjs "^3.0.0" follow-redirects@^1.0.0, follow-redirects@^1.14.7: - version "1.15.1" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.1.tgz#0ca6a452306c9b276e4d3127483e29575e207ad5" - integrity sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA== + version "1.15.6" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.6.tgz#7f815c0cda4249c74ff09e95ef97c23b5fd0399b" + integrity sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA== fork-ts-checker-webpack-plugin@^6.5.0: version "6.5.2" @@ -6239,10 +6244,10 @@ pure-color@^1.2.0: resolved "https://registry.yarnpkg.com/pure-color/-/pure-color-1.3.0.tgz#1fe064fb0ac851f0de61320a8bf796836422f33e" integrity sha1-H+Bk+wrIUfDeYTIKi/eWg2Qi8z4= -qs@6.10.3: - version "6.10.3" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.10.3.tgz#d6cde1b2ffca87b5aa57889816c5f81535e22e8e" - integrity sha512-wr7M2E0OFRfIfJZjKGieI8lBKb7fRCH4Fv5KNPEs7gJ8jadvotdsS08PzOKR7opXhZ/Xkjtt3WF9g38drmyRqQ== +qs@6.11.0: + version "6.11.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.0.tgz#fd0d963446f7a65e1367e01abd85429453f0c37a" + integrity sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q== dependencies: side-channel "^1.0.4" @@ -6275,10 +6280,10 @@ range-parser@^1.2.1, range-parser@~1.2.1: resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== -raw-body@2.5.1: - version "2.5.1" - resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.1.tgz#fe1b1628b181b700215e5fd42389f98b71392857" - integrity sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig== +raw-body@2.5.2: + version "2.5.2" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.2.tgz#99febd83b90e08975087e8f1f9419a149366b68a" + integrity sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA== dependencies: bytes "3.1.2" http-errors "2.0.0" @@ -6409,6 +6414,14 @@ react-loadable-ssr-addon-v5-slorber@^1.0.1: dependencies: "@babel/runtime" "^7.10.3" +"react-loadable@npm:@docusaurus/react-loadable@5.5.2": + version "5.5.2" + resolved "https://registry.yarnpkg.com/@docusaurus/react-loadable/-/react-loadable-5.5.2.tgz#81aae0db81ecafbdaee3651f12804580868fa6ce" + integrity sha512-A3dYjdBGuy0IGT+wyLIGIKLRE+sAk1iNk0f1HjNDysO7u8lhL4N3VEm+FAubmJbAztn94F7MxBTPmnixbiyFdQ== + dependencies: + "@types/react" "*" + prop-types "^15.6.2" + react-router-config@^5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/react-router-config/-/react-router-config-5.1.1.tgz#0f4263d1a80c6b2dc7b9c1902c9526478194a988" @@ -7740,9 +7753,9 @@ webpack-bundle-analyzer@^4.5.0: ws "^7.3.1" webpack-dev-middleware@^5.3.1: - version "5.3.3" - resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-5.3.3.tgz#efae67c2793908e7311f1d9b06f2a08dcc97e51f" - integrity sha512-hj5CYrY0bZLB+eTO+x/j67Pkrquiy7kWepMHmUMoPsmcUaeEnQJqFzHJOyxgWlq746/wUuA64p9ta34Kyb01pA== + version "5.3.4" + resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-5.3.4.tgz#eb7b39281cbce10e104eb2b8bf2b63fce49a3517" + integrity sha512-BVdTqhhs+0IfoeAf7EoH5WE+exCmqGerHfDM0IL096Px60Tq2Mn9MAbnaGUe6HiMa41KMCYF19gyzZmBcq/o4Q== dependencies: colorette "^2.0.10" memfs "^3.4.3" @@ -7927,14 +7940,14 @@ write-file-atomic@^3.0.0: typedarray-to-buffer "^3.1.5" ws@^7.3.1: - version "7.5.9" - resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.9.tgz#54fa7db29f4c7cec68b1ddd3a89de099942bb591" - integrity sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q== + version "7.5.10" + resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.10.tgz#58b5c20dc281633f6c19113f39b349bd8bd558d9" + integrity sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ== ws@^8.4.2: - version "8.8.1" - resolved "https://registry.yarnpkg.com/ws/-/ws-8.8.1.tgz#5dbad0feb7ade8ecc99b830c1d77c913d4955ff0" - integrity sha512-bGy2JzvzkPowEJV++hF07hAD6niYSr0JzBNo/J29WsB57A2r7Wlc1UFcTR9IzrPvuNVO4B8LGqF8qcpsVOhJCA== + version "8.17.1" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.17.1.tgz#9293da530bb548febc95371d90f9c878727d919b" + integrity sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ== xdg-basedir@^4.0.0: version "4.0.0" diff --git a/frontend/.detoxrc.js b/frontend/.detoxrc.js new file mode 100644 index 00000000..c8ad3365 --- /dev/null +++ b/frontend/.detoxrc.js @@ -0,0 +1,55 @@ +/** @type {Detox.DetoxConfig} */ +module.exports = { + testRunner: { + args: { + $0: 'jest', + config: 'e2e/jest.config.js', + }, + jest: { + setupTimeout: 120000, + }, + }, + apps: { + 'ios.debug': { + type: 'ios.app', + binaryPath: 'bin/IOS/insertyourappname.app', + build: + 'xcodebuild -workspace ios/insertyourappname.xcworkspace -scheme insertyourappname -configuration Debug -sdk iphonesimulator -derivedDataPath ios/build | npx excpretty ./', + }, + 'android.debug': { + type: 'android.apk', + binaryPath: 'android/app/build/outputs/apk/debug/app-debug.apk', + build: + 'cd android && ./gradlew assembleDebug assembleAndroidTest -DtestBuildType=debug -Dorg.gradle.jvmargs="-Xmx4g" && cd ..', + }, + }, + devices: { + simulator: { + type: 'ios.simulator', + device: { + type: 'iPhone 14', + }, + }, + emulator: { + type: 'android.emulator', + device: { + avdName: 'Pixel_5_API_28', + }, + }, + }, + configurations: { + 'ios.sim.debug': { + device: 'simulator', + app: 'ios.debug', + }, + 'android.emu.debug': { + device: 'emulator', + app: 'android.debug', + behavior: { + init: { + reinstallApp: false, + }, + }, + }, + }, +}; diff --git a/frontend/.gitignore b/frontend/.gitignore index 4906d099..90211896 100644 --- a/frontend/.gitignore +++ b/frontend/.gitignore @@ -13,6 +13,8 @@ src/graphql/schema.json .env playstore_secret.json dist/ +bin/ +artifacts/ # macOS .DS_Store @@ -20,3 +22,6 @@ dist/ #firebase google-services.json + +android/ +generated/ diff --git a/frontend/Config.mock.ts b/frontend/Config.mock.ts new file mode 100644 index 00000000..f04767d2 --- /dev/null +++ b/frontend/Config.mock.ts @@ -0,0 +1,28 @@ +/** + * This file is created to override the getConfig function from the Config.ts file. + * It will be used when we attempt to run detox tests using a mock server. + * + * For reference on how to handle the mock.ts file, it is set up in the metro.config.js where a condition for sourceExts has been added. + * and for more detail implementation see this docs https://github.com/wix/Detox/blob/master/docs/guide/mocking.md + */ + +import { Platform } from 'react-native'; + +import { RequiredConfig, LocalConfig } from './Config'; +import { MOCK_SERVER_PORT } from './e2e/global'; + +function getConfig(): RequiredConfig | LocalConfig { + return { + proseUrl: + Platform.OS === 'android' + ? /** + * note 10.0.2.2 is ip localhost for android emulator. which we enable traffic in detox configuration for more detail information + * see this detox configuration https://wix.github.io/Detox/docs/introduction/project-setup#43-enabling-unencrypted-traffic-for-detox + * and about localhost ip https://developer.android.com/studio/run/emulator-networking + */ + `http://10.0.2.2:${MOCK_SERVER_PORT}` + : `http://localhost:${MOCK_SERVER_PORT}`, + }; +} + +export default getConfig(); diff --git a/frontend/Config.ts b/frontend/Config.ts index 09e46c4b..9b03795b 100644 --- a/frontend/Config.ts +++ b/frontend/Config.ts @@ -38,6 +38,9 @@ const config: Config = { production: { proseUrl: 'http://PLACEHOLDER.change.this.to.your.prose.url', }, + test: { + proseUrl: 'http://localhost:8929', + }, }, }; @@ -58,8 +61,8 @@ function getConfig(): RequiredConfig | LocalConfig { } /* Type definitions for the `config` object */ -type RequiredConfig = { proseUrl: `${'http' | 'https'}://${string}` }; -type LocalConfig = RequiredConfig & { +export type RequiredConfig = { proseUrl: `${'http' | 'https'}://${string}` }; +export type LocalConfig = RequiredConfig & { inferDevelopmentHost?: boolean; }; type Config = { diff --git a/frontend/app.json b/frontend/app.json index aabfcd9d..298c4090 100644 --- a/frontend/app.json +++ b/frontend/app.json @@ -2,7 +2,7 @@ "expo": { "name": "", "slug": "", - "scheme": "", + "scheme": "your-scheme-here", "currentFullName": "@/", "version": "1.0.0", "orientation": "portrait", @@ -12,6 +12,17 @@ "resizeMode": "contain", "backgroundColor": "#ffffff" }, + "plugins": [ + "expo-localization", + [ + "@config-plugins/detox", + { + "skipProguard": false, + "subdomains": ["localhost", "10.0.2.2"] + } + ], + "./src/plugins/Notification.js" + ], "userInterfaceStyle": "automatic", "updates": { "fallbackToCacheTimeout": 0 @@ -33,20 +44,24 @@ "svg" ] }, + "notification":{ + "icon":"./assets/iconNotifications.png" + }, "ios": { "supportsTablet": false, "buildNumber": "1.0.0", "config": { "usesNonExemptEncryption": false - } + }, + "bundleIdentifier": "com.kfox.insertyourappname" }, "android": { "permissions": [], - "versionCode": 1 + "versionCode": 1, + "package": "com.kfox.insertyourappname" }, "web": { "favicon": "./assets/favicon.png" - }, - "plugins": ["expo-localization"] + } } } diff --git a/frontend/assets/iconNotifications.png b/frontend/assets/iconNotifications.png new file mode 100644 index 00000000..cc89e6d2 Binary files /dev/null and b/frontend/assets/iconNotifications.png differ diff --git a/frontend/assets/icons/BoldText.svg b/frontend/assets/icons/BoldText.svg new file mode 100644 index 00000000..8d8f5b2c --- /dev/null +++ b/frontend/assets/icons/BoldText.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/assets/icons/BulletList.svg b/frontend/assets/icons/BulletList.svg new file mode 100644 index 00000000..2901b86f --- /dev/null +++ b/frontend/assets/icons/BulletList.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/assets/icons/ItalicText.svg b/frontend/assets/icons/ItalicText.svg new file mode 100644 index 00000000..dad87aa2 --- /dev/null +++ b/frontend/assets/icons/ItalicText.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/assets/icons/NumberList.svg b/frontend/assets/icons/NumberList.svg new file mode 100644 index 00000000..e4fdc460 --- /dev/null +++ b/frontend/assets/icons/NumberList.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/assets/icons/QuoteText.svg b/frontend/assets/icons/QuoteText.svg new file mode 100644 index 00000000..6656d4af --- /dev/null +++ b/frontend/assets/icons/QuoteText.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/codegen.ts b/frontend/codegen.ts index 1c62b59b..1c75c9de 100644 --- a/frontend/codegen.ts +++ b/frontend/codegen.ts @@ -25,6 +25,16 @@ const config: CodegenConfig = { 'typescript-react-apollo', ], }, + './e2e/apollo-mock/generated/server.ts': { + plugins: [ + { + add: { + content: '// THIS FILE IS GENERATED, DO NOT EDIT!', + }, + }, + 'typescript', + ], + }, }, hooks: { afterAllFileWrite: [`prettier --write "src/generated/**/*.{ts,tsx}"`], diff --git a/frontend/docs/testing/detox.md b/frontend/docs/testing/detox.md new file mode 100644 index 00000000..10f71171 --- /dev/null +++ b/frontend/docs/testing/detox.md @@ -0,0 +1,197 @@ +# Detox E2E Testing Lexicon + +## Motivation + +Detox is a powerful end-to-end testing tool for mobile apps that provides automated testing capabilities to ensure app quality. The main purpose of using Detox is to tackle bug in app. By simulating user interactions, Detox helps identify bugs and issues early in the development cycle, thereby saving valuable time and effort. + +## Setup Detox + +In order to set up Detox for your project, you will need to install some packages on your computer and project. For macOS users, run the following commands in your terminal: + +``` +brew tap wix/brew +brew install applesimutils +``` + +Next, you will need to install the Detox CLI globally using either Yarn or npm: + +``` +yarn global add detox-cli +# or +npm install detox-cli --global +``` + +In the Lexicon project, the setup differs slightly from the standard Detox documentation as Lexicon uses Expo. To run Detox with Expo, you will need to install the @config-plugins/detox package, which allows for automatic setup of the native project for Detox configuration. You can find this package on npm. + +To set up Detox for Lexicon, follow these steps: + +1. Install the Detox package for lexicon: + + > **Note** + > to install correct detox version check this documentation https://www.npmjs.com/package/@config-plugins/detox + + ``` + yarn add detox --dev + ``` + +2. Install other dependencies required for Detox: + + ``` + yarn add @config-plugins/detox @types/detox --dev + ``` + +3. Add the Detox config plugin to your frontend/app.json file: + + ``` + { + "plugins": ["@config-plugins/detox"] + } + ``` + +4. Run `detox init -r jest` in your terminal. This command will create an e2e folder, which contains the Detox configuration for running tests, as well as the .detoxrc.js file to set up the iOS and Android test file paths and simulator types. + +## Using Graphql Mock In Detox + +For end-to-end (E2E) testing in this project, we make use of a mock GraphQL server. You can find all the setup and mock data in the frontend/e2e/apollo-mock/ directory. If you need to modify the resolver logic, data, or Port for Detox mock tests, you can make the required changes in that folder. + +## Using config mock file + +To differentiate the URL between local development and Detox tests, we created a file called `Config.mock.ts`. This file contains the same function `getProseUrl` as found in `Config.ts`. If the environment variable `DETOX_TESTS` is set to `true` when running `expo star --dev-client` or in package.json script run `yarn start:android:test` (which is required for Android), the function will be overridden with the mock which we create. + +The configuration condition for this behavior is located in the metro.config.js file. + +For more detailed information on how to use and set up this configuration, you can refer to the following link: https://github.com/wix/Detox/blob/master/docs/guide/mocking.md + +## How to run Lexicon's Detox tests locally + +To run Detox tests for Lexicon in your local environment, follow these steps: + +#### iOS + +1. Add the `bundleIdentifier` to your Lexicon project's app.json file. Also, ensure that you have set up other configurations such as the EAS project ID or your project name for building using EAS. + +2. Build your app using the command eas build -p ios --profile=test. If you are using a different channel, make sure to add the simulator configuration to your eas.json file under the iOS section: + + ``` + "ios": { + "simulator": true + } + ``` + +3. After the EAS build is complete, download and unzip the app. + +4. Inside the frontend folder, create a new folder named bin/IOS. + + > **Note** + > For folder name you can change it but don't forget to change the `binaryPath` in file `frontend/.detoxrc.js` + > + > ``` + > { + > "apps":{ + > "ios.debug": { + > "type": "ios.app", + > "binaryPath": "bin/IOS/insertyourappname.app", + > }, + > }, + > } + > ``` + +5. Move the downloaded app into the bin/IOS folder. + +6. Open the frontend/.detoxrc.js file and set the binary path for iOS: + + ``` + { + "apps":{ + "ios.debug": { + "type": "ios.app", + "binaryPath": "bin/IOS/insertyourappname.app", + }, + }, + "configurations": { + "ios.sim.debug": { + "device": "simulator", + "app": "ios.debug" + }, + } + } + ``` + +7. Change the iOS simulator device in the `frontend/.detoxrc.js`n file based on the simulator installed on your local machine: + + ``` + "devices": { + "simulator": { + "type": "ios.simulator", + "device": { + "type": "iPhone 13" + } + }, + } + ``` + +8. Open your terminal, navigate to the frontend directory, and run the command `detox test -c ios.sim.debug`. Wait until the tests finish executing. + +These steps will allow you to run Detox tests for Lexicon on iOS in your local development environment. + +#### android + +1. To run Detox tests locally on Android, you first need to execute the command `expo prebuild --platform android`. + +2. Next, run either of the following commands to build the Detox test suite for Android: `detox build -c android.emu.debug or yarn tests:android:build`. + +3. Update the Android emulator device in the frontend/.detoxrc.js file based on the emulator installed on your local machine. Modify the device configuration as follows: + + ``` + "devices": { + "emulator": { + "type": "android.emulator", + "device": { + "avdName": "Pixel_5_API_28" + } + } + }, + ``` + +4. Open your terminal, navigate to the frontend directory, and run the command `yarn start`. After that open other terminal with directory frontend and run `yarn tests:android:test`. Wait until the tests finish executing. + +## Troubleshooting + +in lexicon there are some need to pay attention about detox test and setup especially for android + +1. if you get error `Detox can't seem to connect to the test app`. can be issue where your android native missing some configuration to run detox. You can check all additional configuration file on android in this [documentation](https://wix.github.io/Detox/docs/introduction/project-setup#step-4-additional-android-configuration). In lexicon project if this configuration missing make sure you already add @config-plugin/detox in app.json + + ``` + { + "plugins": ["@config-plugins/detox"] + } + ``` + +2. Some tests will not work on Android if you are using Emulator API 33, and this issue is still open in this [issue](https://github.com/wix/Detox/issues/3762). Specifically, the typeText and tap functions do not work or may crash the app when attempting to run the test. I suggest trying to use Emulator API 31 or 28, which I have tested and confirmed to work properly. + +3. If you encounter an error stating "No elements found" even though your setup and elements are correct at the start of the test, consider following this suggestion. This issue may occur when running Detox tests in a CI environment. + + To resolve this issue, try opening the simulator first before running the Detox test. This step can help ensure that the necessary elements are loaded properly and available for interaction during the test execution. + + ``` + Test Failed: No elements found for “MATCHER(id == “password_text_input”)” + ``` + +4. if you encounter problem tap test in android. I suggest to check some this thing + +- make sure the button is not hide because keypad or other component because it will make the tap not working +- if you use animation I suggest to add wait time in your test so it will finish load your component before run the test + +5. If you encounter issues connecting to your local API server when running tests, please ensure that you have configured the necessary settings for traffic handling as described in this [traffic configuration guide](https://wix.github.io/Detox/docs/introduction/project-setup#43-enabling-unencrypted-traffic-for-detox). + + Additionally, make sure that you are using the appropriate API URL for local testing with a local API. In this case, you should use http://10.0.2.2 as the URL to access your local API server. + +6. if you want to add CI script for run detox test in android make sure your runner support hardware acceleration to be able run android emulator. if not you will encounter some of this error + + ``` + PANIC: Avd's CPU Architecture 'arm64' is not supported by the QEMU2 emulator on x86_64 host. + + ERROR | x86 emulation currently requires hardware acceleration! + ``` + + when try setup or run emulator diff --git a/frontend/docs/testing/example-Ios-CI.md b/frontend/docs/testing/example-Ios-CI.md new file mode 100644 index 00000000..dd4ba7dd --- /dev/null +++ b/frontend/docs/testing/example-Ios-CI.md @@ -0,0 +1,90 @@ +In order to run iOS simulator for e2e testing, a macOS environment is required. Therefore, we need to use a macOS runner in our CI pipeline. However, it's important to note that using a macOS runner in GitHub Actions consumes approximately 10 times more resources compared to an Ubuntu runner. Due to this increased resource usage, we have made the decision to disable running iOS tests in the CI pipeline to optimize resource allocation. + +```yml +name: E2E test ios + +on: workflow_call + +jobs: + e2e-test-ios: + runs-on: macos-13 + + defaults: + run: + working-directory: ./frontend + + steps: + - name: Checkout + uses: actions/checkout@v3 + + - uses: actions/setup-node@v3 + with: + node-version: '16.x' + + - name: Configure JDK + uses: actions/setup-java@v3 + with: + distribution: 'temurin' + java-version: '11' + + - uses: maxim-lobanov/setup-xcode@v1 + with: + xcode-version: 'latest-stable' + + - name: Get yarn cache directory path + id: yarn-cache-dir-path + run: echo "::set-output name=dir::$(yarn cache dir)" + + - uses: actions/cache@v3 + id: yarn-cache + with: + path: ${{ steps.yarn-cache-dir-path.outputs.dir }} + key: ${{ runner.os }}-yarn-frontend-${{ hashFiles('**/yarn.lock') }} + restore-keys: | + ${{ runner.os }}-yarn-frontend- + + - name: Setup EAS + uses: expo/expo-github-action@v8 + with: + eas-version: latest + expo-version: latest + token: ${{ secrets.EXPO_TOKEN }} + + - name: Install Dependencies + run: yarn install --frozen-lockfile + + - name: EAS build test + if: ${{ env.BUILD_APP == 'TRUE' }} + run: eas build -p ios --profile=test --non-interactive + + - name: Get current build url + run: | + JSON=$(eas build:list --distribution=simulator --status=finished --platform=ios --limit=1 --json --non-interactive) + URL=$(echo $JSON | jq -r '.[0].artifacts.buildUrl') + echo "::set-output name=BUILD_URL::$URL" + id: url + + - name: Create target directory + run: mkdir -p bin/IOS/ + + - name: Download build + id: downloadedApp + run: curl -o bin/IOS/build.tar.gz -L "${{ steps.url.outputs.BUILD_URL }}" + + - name: Unzip app build + run: | + cd bin/IOS/ + ls -a + for f in *.tar.gz; do tar -xvf "$f"; done + ls + cd ../.. + + - name: Setup Detox + run: | + brew tap wix/brew + brew install applesimutils + npm install -g detox-cli + + - name: Run Detox tests + run: yarn tests:ios:test +``` diff --git a/frontend/e2e/apollo-mock/assets/grinning.png b/frontend/e2e/apollo-mock/assets/grinning.png new file mode 100644 index 00000000..e864c216 Binary files /dev/null and b/frontend/e2e/apollo-mock/assets/grinning.png differ diff --git a/frontend/e2e/apollo-mock/assets/heart_eyes.png b/frontend/e2e/apollo-mock/assets/heart_eyes.png new file mode 100644 index 00000000..9123e055 Binary files /dev/null and b/frontend/e2e/apollo-mock/assets/heart_eyes.png differ diff --git a/frontend/e2e/apollo-mock/assets/smile.png b/frontend/e2e/apollo-mock/assets/smile.png new file mode 100644 index 00000000..e032945e Binary files /dev/null and b/frontend/e2e/apollo-mock/assets/smile.png differ diff --git a/frontend/e2e/apollo-mock/data/categories.ts b/frontend/e2e/apollo-mock/data/categories.ts new file mode 100644 index 00000000..e3e5ae7e --- /dev/null +++ b/frontend/e2e/apollo-mock/data/categories.ts @@ -0,0 +1,16 @@ +export const mockCategories = [ + { + id: 2, + color: '25AAE2', + name: 'General', + descriptionText: + 'Create topics here that don’t fit into any other existing category.', + }, + { + id: 3, + color: '231F20', + name: 'Lexicon UAT', + descriptionText: + 'Use this category for all testing purposes related to Lexicon. Whether you’re testing certain features like push notifications & email deep linking—or just testing that posts can still be created correctly—please make sure you assign to this category.', + }, +]; diff --git a/frontend/e2e/apollo-mock/data/index.ts b/frontend/e2e/apollo-mock/data/index.ts new file mode 100644 index 00000000..30f85d91 --- /dev/null +++ b/frontend/e2e/apollo-mock/data/index.ts @@ -0,0 +1,9 @@ +export * from './users'; +export * from './topics'; +export * from './categories'; +export * from './topicDetails'; +export * from './messages'; +export * from './messageDetails'; +export * from './token'; +export * from './posts'; +export * from './site'; diff --git a/frontend/e2e/apollo-mock/data/messageDetails.ts b/frontend/e2e/apollo-mock/data/messageDetails.ts new file mode 100644 index 00000000..f709f17e --- /dev/null +++ b/frontend/e2e/apollo-mock/data/messageDetails.ts @@ -0,0 +1,24 @@ +import { mockMessages } from './messages'; +import { mockMessageReplies } from './posts'; +import { mockUsers } from './users'; + +export let mockMessageDetails = [ + { + id: mockMessages[0].id, + title: 'Testing new message', + postStream: { + posts: [mockMessageReplies[0]], + stream: [mockMessageReplies[0].id], + firstPost: null, + }, + details: { + allowedUsers: mockUsers, + participants: [mockUsers[1]], + createdBy: { + id: mockUsers[1].id, + username: mockUsers[1].username, + }, + }, + postsCount: mockMessages[0].postsCount, + }, +]; diff --git a/frontend/e2e/apollo-mock/data/messages.ts b/frontend/e2e/apollo-mock/data/messages.ts new file mode 100644 index 00000000..413a5853 --- /dev/null +++ b/frontend/e2e/apollo-mock/data/messages.ts @@ -0,0 +1,40 @@ +import { mockNewMessageTopic } from './topics'; +import { mockUsers } from './users'; + +export const mockMessages = [ + { + id: 6, + title: 'Testing new message', + unseen: false, + allowedUserCount: 2, + highestPostNumber: 1, + lastReadPostNumber: 1, + lastPostedAt: '2023-05-23T05:23:16.062Z', + lastPosterUsername: mockUsers[1].username, + participants: [ + { + userId: mockUsers[1].id, + extras: 'latest', + }, + ], + postsCount: 1, + }, +]; + +export const mockNewMessage = { + id: 11, + title: mockNewMessageTopic.title, + unseen: false, + allowedUserCount: 2, + highestPostNumber: 1, + lastReadPostNumber: 1, + lastPostedAt: mockNewMessageTopic.createdAt, + lastPosterUsername: mockUsers[0].username, + participants: [ + { + userId: mockUsers[1].id, + extras: 'latest', + }, + ], + postsCount: mockNewMessageTopic.postsCount, +}; diff --git a/frontend/e2e/apollo-mock/data/posts.ts b/frontend/e2e/apollo-mock/data/posts.ts new file mode 100644 index 00000000..d0c479c8 --- /dev/null +++ b/frontend/e2e/apollo-mock/data/posts.ts @@ -0,0 +1,299 @@ +import { mockMessages } from './messages'; +import { mockNewTopicWithPoll, mockTopics } from './topics'; +import { mockUsers } from './users'; + +export const mockPostsReplies = [ + { + id: 4, + topicId: mockTopics[2].id, + username: mockUsers[0].username, + actionCode: null, + actionCodeWho: null, + avatarTemplate: mockUsers[0].avatarTemplate, + hidden: false, + canEdit: true, + markdownContent: 'Just want to reply in here', + mentions: null, + createdAt: '2023-07-13T07:40:18.723Z', + updatedAt: '2023-07-13T07:40:18.723Z', + replyCount: 0, + actionsSummary: [ + { + id: 2, + count: null, + acted: null, + }, + { + id: 3, + count: null, + acted: null, + }, + { + id: 4, + count: null, + acted: null, + }, + { + id: 8, + count: null, + acted: null, + }, + { + id: 6, + count: null, + acted: null, + }, + { + id: 7, + count: null, + acted: null, + }, + ], + postNumber: mockTopics[2].postsCount, + replyToPostNumber: null, + userStatus: mockUsers[0].status, + }, + { + id: 5, + topicId: mockTopics[2].id, + username: mockUsers[0].username, + actionCode: null, + actionCodeWho: null, + avatarTemplate: mockUsers[0].avatarTemplate, + hidden: false, + canEdit: true, + markdownContent: 'Sending a reply. [test url](https://www.google.com)', + mentions: null, + createdAt: '2023-07-13T07:40:18.723Z', + replyCount: 0, + actionsSummary: [ + { + id: 2, + count: null, + acted: null, + }, + { + id: 3, + count: null, + acted: null, + }, + { + id: 4, + count: null, + acted: null, + }, + { + id: 8, + count: null, + acted: null, + }, + { + id: 6, + count: null, + acted: null, + }, + { + id: 7, + count: null, + acted: null, + }, + ], + postNumber: mockTopics[2].postsCount + 1, + replyToPostNumber: null, + userStatus: mockUsers[0].status, + }, + { + id: 13, + topicId: mockTopics[2].id, + username: mockUsers[0].username, + actionCode: null, + actionCodeWho: null, + avatarTemplate: mockUsers[0].avatarTemplate, + hidden: false, + canEdit: true, + markdownContent: 'This is a quote reply.', + mentions: null, + createdAt: '2023-07-13T07:40:18.723Z', + replyCount: 0, + actionsSummary: [ + { + id: 2, + count: null, + acted: null, + }, + { + id: 3, + count: null, + acted: null, + }, + { + id: 4, + count: null, + acted: null, + }, + { + id: 8, + count: null, + acted: null, + }, + { + id: 6, + count: null, + acted: null, + }, + { + id: 7, + count: null, + acted: null, + }, + ], + postNumber: mockTopics[2].postsCount + 2, + replyToPostNumber: mockTopics[2].postsCount, + userStatus: mockUsers[0].status, + }, +]; + +export const mockMessageReplies = [ + { + id: 7, + topicId: mockMessages[0].id, + username: mockUsers[1].username, + actionCode: null, + actionCodeWho: null, + markdownContent: 'Testing new message push.', + mentions: null, + createdAt: '2023-05-23T05:23:16.062Z', + postNumber: 1, + }, + { + id: 8, + topicId: mockMessages[0].id, + username: mockUsers[0].username, + actionCode: null, + actionCodeWho: null, + markdownContent: 'Test sending a reply.', + mentions: null, + createdAt: '2023-05-24T05:23:16.062Z', + postNumber: 2, + }, + { + id: 9, + topicId: mockMessages[0].id, + username: mockUsers[0].username, + actionCode: null, + actionCodeWho: null, + markdownContent: '[test url](https://www.google.com)', + mentions: null, + createdAt: '2023-05-24T05:23:16.062Z', + postNumber: 2, + }, +]; + +export const mockFirstPost = { + id: 3, + topicId: mockTopics[2].id, + username: mockUsers[0].username, + actionCode: null, + actionCodeWho: null, + avatarTemplate: mockUsers[0].avatarTemplate, + hidden: false, + canEdit: true, + markdownContent: mockTopics[2].excerpt, + mentions: [], + createdAt: mockTopics[2].createdAt, + replyCount: mockTopics[2].replyCount, + postNumber: 1, + replyToPostNumber: null, + userStatus: mockUsers[0].status, + actionsSummary: mockPostsReplies[0].actionsSummary, +}; + +export const mockPostWithPoll = { + id: 12, + topicId: mockNewTopicWithPoll.id, + username: mockUsers[0].username, + actionCode: null, + actionCodeWho: null, + avatarTemplate: mockUsers[0].avatarTemplate, + hidden: false, + canEdit: true, + raw: '[poll type=regular results=always public=true chartType=bar]\n# Favorite Fruit\n* Banana\n* Apple\n* Mango\n[/poll]', + markdownContent: + '[poll type=regular results=always public=true chartType=bar]\n# Favorite Fruit\n* Banana\n* Apple\n* Mango\n[/poll]', + mentions: [], + createdAt: '2023-07-13T07:40:18.723Z', + updatedAt: '2023-07-13T07:40:18.723Z', + replyCount: 0, + actionsSummary: [ + { + id: 2, + count: null, + acted: null, + }, + { + id: 3, + count: null, + acted: null, + }, + { + id: 4, + count: null, + acted: null, + }, + { + id: 8, + count: null, + acted: null, + }, + { + id: 6, + count: null, + acted: null, + }, + { + id: 7, + count: null, + acted: null, + }, + ], + postNumber: mockNewTopicWithPoll.postsCount, + replyToPostNumber: null, + polls: [ + { + name: 'poll', + type: 'regular', + status: 'open', + results: 'always', + options: [ + { + id: 'fd10f08e2f91c68a6489a4f0d475cea2', + html: 'Banana', + votes: 0, + }, + { + id: '4341f33f3bc598443053955f68b9a24f', + html: 'Apple', + votes: 0, + }, + { + id: '9414c67b0ad5577e3121a5e56f7a0176', + html: 'Mango', + votes: 0, + }, + ], + voters: 0, + chartType: 'bar', + title: 'Favorite Fruit', + groups: null, + public: true, + min: null, + max: null, + close: null, + step: null, + preloadedVoters: [], + }, + ], + pollsVotes: null, +}; + +export const allPosts = [...mockPostsReplies, mockFirstPost, mockPostWithPoll]; diff --git a/frontend/e2e/apollo-mock/data/site.ts b/frontend/e2e/apollo-mock/data/site.ts new file mode 100644 index 00000000..f1c185e7 --- /dev/null +++ b/frontend/e2e/apollo-mock/data/site.ts @@ -0,0 +1,34 @@ +import { MOCK_SERVER_PORT } from '../../global'; + +import { mockCategories } from './categories'; + +export const siteSetting = { + canCreateTag: true, + canTagTopics: true, + canSignUp: false, + authorizedExtensions: '', + uncategorizedCategoryId: 1, + minSearchLength: 3, + taggingEnabled: true, + maxTagLength: 10, + maxTagsPerTopic: 5, + maxUsernameLength: 15, + minUsernameLength: 1, + minPasswordLength: 2, + fullNameRequired: false, + topicFlagTypes: [], + postActionTypes: [], + allowUncategorizedTopics: false, + defaultComposerCategory: mockCategories[0].id, + groups: [], + allowUserStatus: true, + emojiSet: 'twitter', + externalEmojiUrl: '', + /** + * This local url used for android + */ + discourseBaseUrl: `http://10.0.2.2:${MOCK_SERVER_PORT}`, + allowPoll: true, + pollCreateMinimumTrustLevel: 1, + enableLexiconPushNotifications: false, +}; diff --git a/frontend/e2e/apollo-mock/data/token.ts b/frontend/e2e/apollo-mock/data/token.ts new file mode 100644 index 00000000..eef12e64 --- /dev/null +++ b/frontend/e2e/apollo-mock/data/token.ts @@ -0,0 +1,2 @@ +export const mockToken = '1234567890'; +export const emailToken = 'abcdef123456'; diff --git a/frontend/e2e/apollo-mock/data/topicDetails.ts b/frontend/e2e/apollo-mock/data/topicDetails.ts new file mode 100644 index 00000000..aa993682 --- /dev/null +++ b/frontend/e2e/apollo-mock/data/topicDetails.ts @@ -0,0 +1,69 @@ +import { mockFirstPost, mockPostWithPoll, mockPostsReplies } from './posts'; +import { mockNewTopicWithPoll, mockNewTopics, mockTopics } from './topics'; +import { mockUsers } from './users'; + +export let mockTopicDetails = [ + { + id: mockTopics[2].id, + title: mockTopics[2].title, + views: mockTopics[2].views, + likeCount: mockTopics[2].likeCount, + postsCount: mockTopics[2].postsCount, + liked: mockTopics[2].liked, + categoryId: mockTopics[2].categoryId, + tags: mockTopics[2].tags, + createdAt: mockTopics[2].createdAt, + postStream: { + posts: [mockPostsReplies[0]], + stream: [mockPostsReplies[0].id], + firstPost: mockFirstPost, + }, + details: { + canEdit: true, + allowedUsers: null, + participants: [mockUsers[0]], + }, + }, + { + id: mockNewTopics.id, + title: mockNewTopics.title, + views: mockNewTopics.views, + likeCount: mockNewTopics.likeCount, + postsCount: mockNewTopics.postsCount, + liked: mockNewTopics.liked, + categoryId: mockNewTopics.categoryId, + tags: mockNewTopics.tags, + createdAt: mockNewTopics.createdAt, + postStream: { + posts: [], + stream: [], + firstPost: null, + }, + details: { + canEdit: true, + allowedUsers: null, + participants: [mockUsers[0]], + }, + }, + { + id: mockNewTopicWithPoll.id, + title: mockNewTopicWithPoll.title, + views: mockNewTopicWithPoll.views, + likeCount: mockNewTopicWithPoll.likeCount, + postsCount: mockNewTopicWithPoll.postsCount, + liked: mockNewTopicWithPoll.liked, + categoryId: mockNewTopicWithPoll.categoryId, + tags: mockNewTopicWithPoll.tags, + createdAt: mockNewTopicWithPoll.createdAt, + postStream: { + posts: [], + stream: [mockPostWithPoll.id], + firstPost: mockPostWithPoll, + }, + details: { + canEdit: true, + allowedUsers: null, + participants: [mockUsers[0]], + }, + }, +]; diff --git a/frontend/e2e/apollo-mock/data/topics.ts b/frontend/e2e/apollo-mock/data/topics.ts new file mode 100644 index 00000000..5edaa95b --- /dev/null +++ b/frontend/e2e/apollo-mock/data/topics.ts @@ -0,0 +1,270 @@ +import { mockCategories } from './categories'; +import { mockUsers } from './users'; + +export const mockTopics = [ + { + id: 1, + title: 'Welcome to Lexicon Lounge', + imageUrl: null, + postsCount: 1, + replyCount: 0, + createdAt: '2023-01-05T15:55:22.000Z', + bumpedAt: '2023-01-05T15:55:22.000Z', + excerpt: + 'Welcome to the Lounge\n\nA community for technical discussions and Q&A related to software engineering and technology in general. \n\nThis is a Civilized Place for Public Discussion\n\nPlease treat this discussion forum with…', + visible: true, + liked: false, + tags: [], + views: 15, + likeCount: 0, + categoryId: mockCategories[0].id, + posters: [ + { + userId: mockUsers[0].id, + description: 'Original Poster, Most Recent Poster', + user: mockUsers[0], + }, + ], + postersUnion: [ + { + userId: mockUsers[0].id, + description: 'Original Poster, Most Recent Poster', + user: mockUsers[0], + }, + ], + authorUserId: mockUsers[0].id, + frequentPosterUserId: null, + pinned: true, + }, + { + id: 2, + title: 'Hotel or resort in Ubud', + imageUrl: null, + postsCount: 2, + replyCount: 0, + createdAt: '2023-05-11T04:55:20.324Z', + bumpedAt: '2023-05-24T09:58:23.020Z', + excerpt: + 'Do you guys have any preference hotel or resort or villa in Ubud? with a cheap prive ofc', + visible: true, + liked: false, + tags: [], + views: 11, + likeCount: 1, + categoryId: mockCategories[0].id, + posters: [ + { + userId: mockUsers[1].id, + description: 'Original Poster', + user: mockUsers[1], + }, + { + userId: mockUsers[0].id, + description: 'Most Recent Poster', + user: mockUsers[0], + }, + ], + postersUnion: [ + { + userId: mockUsers[1].id, + description: 'Original Poster', + user: mockUsers[1], + }, + { + userId: mockUsers[0].id, + description: 'Most Recent Poster', + user: mockUsers[0], + }, + ], + authorUserId: mockUsers[0].id, + frequentPosterUserId: null, + pinned: false, + }, + { + id: 3, + title: 'Detox Test', + imageUrl: null, + postsCount: 2, + replyCount: 0, + createdAt: '2023-07-10T09:41:35.301Z', + bumpedAt: '2023-07-10T19:29:25.000Z', + excerpt: 'For testing Detox', + visible: true, + liked: false, + tags: [], + views: 2, + likeCount: 0, + categoryId: mockCategories[1].id, + posters: [ + { + userId: mockUsers[0].id, + description: 'Original Poster, Most Recent Poster', + user: mockUsers[0], + }, + ], + postersUnion: [ + { + userId: mockUsers[0].id, + description: 'Original Poster, Most Recent Poster', + user: mockUsers[0], + }, + ], + authorUserId: mockUsers[0].id, + frequentPosterUserId: null, + pinned: false, + }, +]; + +export const mockNewTopics = { + id: 9, + title: 'Creating a New Post', + imageUrl: null, + postsCount: 1, + replyCount: 0, + createdAt: '2023-07-10T09:41:35.301Z', + bumpedAt: '2023-07-10T19:29:25.000Z', + excerpt: 'Adding a content to the new post.', + visible: true, + liked: false, + tags: [], + views: 2, + likeCount: 0, + categoryId: mockCategories[1].id, + posters: [ + { + userId: mockUsers[0].id, + description: 'Original Poster, Most Recent Poster', + user: mockUsers[0], + }, + ], + postersUnion: [ + { + userId: mockUsers[0].id, + description: 'Original Poster, Most Recent Poster', + user: mockUsers[0], + }, + ], + authorUserId: mockUsers[0].id, + frequentPosterUserId: null, + pinned: false, +}; + +export const mockNewMessageTopic = { + id: 10, + title: 'Send a new message', + imageUrl: null, + postsCount: 1, + replyCount: 0, + createdAt: '2023-09-11T02:30:02.637Z', + bumpedAt: '2023-09-11T02:30:02.637Z', + excerpt: 'I am sending this message to you.', + visible: true, + liked: false, + tags: [], + views: 1, + likeCount: 0, + categoryId: null, + posters: [ + { + userId: mockUsers[0].id, + description: 'Original Poster, Most Recent Poster', + user: mockUsers[0], + }, + ], + postersUnion: [ + { + userId: mockUsers[0].id, + description: 'Original Poster, Most Recent Poster', + user: mockUsers[0], + }, + ], + authorUserId: mockUsers[0].id, + frequentPosterUserId: null, + pinned: false, +}; + +export const mockNewTopicWithPoll = { + id: 11, + title: 'New Post with Poll', + imageUrl: null, + postsCount: 1, + replyCount: 0, + createdAt: '2023-07-11T09:41:35.301Z', + bumpedAt: '2023-07-11T19:29:25.000Z', + excerpt: 'poll', + visible: true, + liked: false, + tags: [], + views: 2, + likeCount: 0, + categoryId: mockCategories[1].id, + posters: [ + { + userId: mockUsers[0].id, + description: 'Original Poster, Most Recent Poster', + user: mockUsers[0], + }, + ], + postersUnion: [ + { + userId: mockUsers[0].id, + description: 'Original Poster, Most Recent Poster', + user: mockUsers[0], + }, + ], + authorUserId: mockUsers[0].id, + frequentPosterUserId: null, + pinned: false, +}; + +export const mockSearchTopics = { + posts: [ + { + id: 12, + username: mockUsers[0].username, + avatarTemplate: mockUsers[0].avatarTemplate, + createdAt: mockTopics[0].createdAt, + likeCount: mockTopics[0].likeCount, + blurb: + '#welcome Welcome to Lounge A community for technical discussions and Q & A related to software engineering and technology in general. #civilized This is a...', + postNumber: 1, + topicId: mockTopics[0].id, + }, + { + id: 13, + username: mockUsers[0].username, + avatarTemplate: mockUsers[0].avatarTemplate, + createdAt: mockTopics[1].createdAt, + likeCount: mockTopics[1].likeCount, + blurb: mockTopics[1].excerpt, + postNumber: 1, + topicId: mockTopics[1].id, + }, + { + id: '3', + username: mockUsers[0].username, + avatarTemplate: mockUsers[0].avatarTemplate, + createdAt: mockTopics[2].createdAt, + likeCount: mockTopics[2].likeCount, + blurb: mockTopics[2].excerpt, + postNumber: 1, + topicId: mockTopics[2].id, + }, + ], + topics: [ + { + ...mockTopics[0], + archetype: 'regular', + }, + { + ...mockTopics[1], + archetype: 'regular', + }, + { + ...mockTopics[2], + archetype: 'regular', + }, + ], +}; + +export const allTopics = [...mockTopics, mockNewTopics, mockNewTopicWithPoll]; diff --git a/frontend/e2e/apollo-mock/data/users.ts b/frontend/e2e/apollo-mock/data/users.ts new file mode 100644 index 00000000..e526975a --- /dev/null +++ b/frontend/e2e/apollo-mock/data/users.ts @@ -0,0 +1,176 @@ +import { UserDetail } from '../generated/server'; + +export const mockUsers: Array = [ + { + id: 1, + username: 'johndoe', + name: 'John Doe', + avatarTemplate: 'https://cdn-icons-png.flaticon.com/512/219/219986.png', + admin: false, + moderator: false, + muted: false, + createdAt: '2023-01-07T08:17:49.000Z', + badgeCount: 13, + ignored: false, + mailingListPostsPerDay: 1, + pendingCount: 0, + profileViewCount: 22, + recentTimeRead: 17303, + timeRead: 29782, + trustLevel: 3, + canEdit: true, + canEditEmail: true, + canEditName: true, + canEditUsername: false, + canIgnoreUser: false, + canMuteUser: false, + canSendPrivateMessages: true, + canSendPrivateMessageToUser: false, + canUploadProfileHeader: true, + canUploadUserCardBackground: true, + groups: [], + allowedPmUsernames: [], + canChangeBio: true, + canChangeLocation: true, + canChangeWebsite: true, + groupUsers: [], + hasTitleBadges: false, + ignoredUsernames: [], + email: '', + mutedCategoryIds: [], + mutedTags: [], + mutedUsernames: [], + regularCategoryIds: [], + secondFactorEnabled: true, + systemAvatarTemplate: '', + trackedCategoryIds: [], + trackedTags: [], + userAuthTokens: [], + watchedCategoryIds: [], + watchedFirstPostCategoryIds: [], + watchedTags: [], + watchingFirstPostTags: [], + userOption: { + allowPrivateMessages: true, + autoTrackTopicsAfterMsecs: 0, + automaticallyUnpinTopics: false, + digestAfterMinutes: 0, + dynamicFavicon: false, + emailDigests: true, + emailInReplyTo: true, + emailLevel: 0, + emailMessagesLevel: 0, + emailPreviousReplies: 0, + enableAllowedPmUsers: true, + enableDefer: true, + enableQuoting: true, + externalLinksInNewTab: true, + hideProfileAndPresence: true, + includeTl0InDigests: false, + likeNotificationFrequency: 0, + mailingListMode: false, + mailingListModeFrequency: 0, + newTopicDurationMinutes: 10, + notificationLevelWhenReplying: 0, + skipNewUserTips: true, + textSize: '', + textSizeSeq: 0, + themeIds: [], + themeKeySeq: 0, + timezone: '', + titleCountMode: '', + userId: 0, + }, + status: { + description: 'Test Status', + emoji: ':heart_eyes:', + endsAt: null, + }, + }, + { + id: 2, + username: 'maryfitz', + name: 'Mary Fitz', + avatarTemplate: 'https://cdn-icons-png.flaticon.com/512/194/194938.png', + admin: false, + moderator: false, + muted: false, + createdAt: '2023-01-07T08:17:49.000Z', + badgeCount: 13, + ignored: false, + mailingListPostsPerDay: 1, + pendingCount: 0, + profileViewCount: 22, + recentTimeRead: 17303, + timeRead: 29782, + trustLevel: 3, + canEdit: true, + canEditEmail: true, + canEditName: true, + canEditUsername: false, + canIgnoreUser: false, + canMuteUser: false, + canSendPrivateMessages: true, + canSendPrivateMessageToUser: false, + canUploadProfileHeader: true, + canUploadUserCardBackground: true, + groups: [], + allowedPmUsernames: [], + canChangeBio: true, + canChangeLocation: true, + canChangeWebsite: true, + groupUsers: [], + hasTitleBadges: false, + ignoredUsernames: [], + email: '', + mutedCategoryIds: [], + mutedTags: [], + mutedUsernames: [], + regularCategoryIds: [], + secondFactorEnabled: true, + systemAvatarTemplate: '', + trackedCategoryIds: [], + trackedTags: [], + userAuthTokens: [], + watchedCategoryIds: [], + watchedFirstPostCategoryIds: [], + watchedTags: [], + watchingFirstPostTags: [], + userOption: { + allowPrivateMessages: true, + autoTrackTopicsAfterMsecs: 0, + automaticallyUnpinTopics: false, + digestAfterMinutes: 0, + dynamicFavicon: false, + emailDigests: true, + emailInReplyTo: true, + emailLevel: 0, + emailMessagesLevel: 0, + emailPreviousReplies: 0, + enableAllowedPmUsers: true, + enableDefer: true, + enableQuoting: true, + externalLinksInNewTab: true, + hideProfileAndPresence: true, + includeTl0InDigests: false, + likeNotificationFrequency: 0, + mailingListMode: false, + mailingListModeFrequency: 0, + newTopicDurationMinutes: 10, + notificationLevelWhenReplying: 0, + skipNewUserTips: true, + textSize: '', + textSizeSeq: 0, + themeIds: [], + themeKeySeq: 0, + timezone: '', + titleCountMode: '', + userId: 0, + }, + status: { + description: '', + emoji: '', + endsAt: null, + }, + }, +]; diff --git a/frontend/e2e/apollo-mock/resolvers/categoriesResolver.ts b/frontend/e2e/apollo-mock/resolvers/categoriesResolver.ts new file mode 100644 index 00000000..c5c89f80 --- /dev/null +++ b/frontend/e2e/apollo-mock/resolvers/categoriesResolver.ts @@ -0,0 +1,11 @@ +import { mockCategories } from '../data'; + +export const categoriesResolvers = { + Query: { + category: () => { + return { + categories: mockCategories, + }; + }, + }, +}; diff --git a/frontend/e2e/apollo-mock/resolvers/index.ts b/frontend/e2e/apollo-mock/resolvers/index.ts new file mode 100644 index 00000000..81212291 --- /dev/null +++ b/frontend/e2e/apollo-mock/resolvers/index.ts @@ -0,0 +1,12 @@ +export * from './loginResolvers'; +export * from './topicsResolvers'; +export * from './categoriesResolver'; +export * from './queriesResolvers'; +export * from './mutationsResolver'; +export * from './messageResolver'; +export * from './profileResolver'; +export * from './siteResolver'; +export * from './userResolvers'; +export * from './pollResolvers'; +export * from './userStatusResolver'; +export * from './userActivityResolver'; diff --git a/frontend/e2e/apollo-mock/resolvers/loginResolvers.ts b/frontend/e2e/apollo-mock/resolvers/loginResolvers.ts new file mode 100644 index 00000000..254f2dc5 --- /dev/null +++ b/frontend/e2e/apollo-mock/resolvers/loginResolvers.ts @@ -0,0 +1,63 @@ +import { mockUsers, mockToken, emailToken } from '../data'; +import { LoginOutputUnion } from '../generated/server'; + +export const loginResolvers = { + LoginOutputUnion: { + __resolveType(obj: LoginOutputUnion) { + if ('user' in obj && 'token' in obj) { + return 'LoginOutput'; + } else if ( + 'secondFactorRequired' in obj && + 'error' in obj && + 'reason' in obj + ) { + return 'SecondFactorRequired'; + } + return null; // Return null if the type cannot be resolved + }, + }, + + Mutation: { + login: ( + _: unknown, + { email, password }: { email: string; password: string }, + ) => { + // Perform your mock login logic here + if (email === 'test@example.com' && password === 'password') { + return { + token: mockToken, + user: mockUsers[0], + enableLexiconPushNotifications: false, + }; + } else { + throw new Error('Incorrect username, email or password'); + } + }, + authenticateLoginLink: (_: unknown, { token }: { token: string }) => { + if (token === emailToken) { + return { + token: mockToken, + user: mockUsers[0], + enableLexiconPushNotifications: false, + }; + } + throw new Error('Sorry link is not valid'); + }, + activateAccount: (_: unknown, { token }: { token: string }) => { + if (token === emailToken) { + return { + token: mockToken, + user: mockUsers[0], + enableLexiconPushNotifications: false, + }; + } + throw new Error('Sorry link is not valid'); + }, + logout: ( + _: unknown, + __: { username: string; pushNotificationsToken: string }, + ) => { + return 'success'; + }, + }, +}; diff --git a/frontend/e2e/apollo-mock/resolvers/messageResolver.ts b/frontend/e2e/apollo-mock/resolvers/messageResolver.ts new file mode 100644 index 00000000..76a2aa22 --- /dev/null +++ b/frontend/e2e/apollo-mock/resolvers/messageResolver.ts @@ -0,0 +1,73 @@ +import { mockNewMessageTopic, mockUsers } from '../data'; +import { mockMessageDetails } from '../data/messageDetails'; +import { mockMessages, mockNewMessage } from '../data/messages'; + +export const messageResolvers = { + Query: { + privateMessage: (_: unknown, { username }: { username: string }) => { + if (username) { + return { + primaryGroups: [], + topicList: { + topics: mockMessages, + }, + users: mockUsers, + }; + } + throw new Error('User not found'); + }, + privateMessageDetail: ( + _: unknown, + { + topicId, + }: { + topicId: number; + postIds: Array; + postNumber: number; + includeFirstPost: boolean; + }, + ) => { + let messageDetails = mockMessageDetails.filter((t) => t.id === topicId); + + if (messageDetails.length === 0) { + throw new Error('Message not found'); + } + + return messageDetails[0]; + }, + }, + Mutation: { + newPrivateMessage: (_: unknown) => { + mockMessages.push(mockNewMessage); + + return { + id: mockNewMessage.id, + name: mockNewMessageTopic.posters[0].user.name, + username: mockNewMessageTopic.posters[0].user.username, + avatarTemplate: mockNewMessageTopic.posters[0].user.avatarTemplate, + createdAt: mockNewMessageTopic.createdAt, + raw: mockNewMessageTopic.excerpt, + postNumber: 1, + postType: 1, + replyCount: 0, + replyToPostNumber: null, + topicId: mockNewMessageTopic.id, + displayUsername: mockNewMessageTopic.posters[0].user.name, + canEdit: true, + moderator: false, + admin: false, + staff: false, + userId: mockNewMessageTopic.posters[0].user.id, + }; + }, + leaveMessage: (_: unknown, { topicId }: { topicId: number }) => { + const indexToRemove = mockMessages.findIndex(({ id }) => id === topicId); + + if (indexToRemove !== -1) { + mockMessages.splice(indexToRemove, 1); + } + + return 'success'; + }, + }, +}; diff --git a/frontend/e2e/apollo-mock/resolvers/mutationsResolver.ts b/frontend/e2e/apollo-mock/resolvers/mutationsResolver.ts new file mode 100644 index 00000000..a96e50a6 --- /dev/null +++ b/frontend/e2e/apollo-mock/resolvers/mutationsResolver.ts @@ -0,0 +1,10 @@ +export const mutationsResolvers = { + Mutation: { + timings: ( + _: unknown, + __: { postNumbers: Array; topicId: number }, + ) => { + return 'success'; + }, + }, +}; diff --git a/frontend/e2e/apollo-mock/resolvers/pollResolvers.ts b/frontend/e2e/apollo-mock/resolvers/pollResolvers.ts new file mode 100644 index 00000000..e70b83d3 --- /dev/null +++ b/frontend/e2e/apollo-mock/resolvers/pollResolvers.ts @@ -0,0 +1,77 @@ +import { PollStatus } from '../../../src/generated/server'; +import { mockPostWithPoll, mockUsers } from '../data'; + +export const pollResolvers = { + Mutation: { + votePoll: ( + _: unknown, + { + options, + }: { + postId: number; + pollName: string; + options: Array; + }, + ) => { + const polls = mockPostWithPoll.polls || []; + const poll = polls[0]; + const newOptions = poll.options.map((option) => { + if (option.id === options[0]) { + return { + ...option, + votes: option.votes + 1, + }; + } + return option; + }); + + return { + poll: { + ...poll, + options: newOptions, + preloadedVoters: [ + { + pollOptionId: options[0], + users: [mockUsers[0]], + }, + ], + voters: poll.voters + 1, + }, + vote: options, + }; + }, + undoVotePoll: (_: unknown) => { + const polls = mockPostWithPoll.polls || []; + const poll = polls[0]; + const newOptions = poll.options.map((option) => { + return { + ...option, + votes: 0, + }; + }); + + return { + ...poll, + options: newOptions, + preloadedVoters: [], + }; + }, + togglePollStatus: ( + _: unknown, + { + status, + }: { + postId: number; + pollName: string; + status: PollStatus; + }, + ) => { + const polls = mockPostWithPoll.polls || []; + + return { + ...polls[0], + status, + }; + }, + }, +}; diff --git a/frontend/e2e/apollo-mock/resolvers/profileResolver.ts b/frontend/e2e/apollo-mock/resolvers/profileResolver.ts new file mode 100644 index 00000000..f1300dd3 --- /dev/null +++ b/frontend/e2e/apollo-mock/resolvers/profileResolver.ts @@ -0,0 +1,71 @@ +import { mockUsers } from '../data'; +import { EditProfileInput, UserUnion } from '../generated/server'; + +export const profileResolvers = { + UserUnion: { + __resolveType(obj: UserUnion) { + if ('email' in obj) { + return 'UserDetail'; + } + return 'UserLite'; + }, + }, + Query: { + userProfile: (_: unknown, { username }: { username: string }) => { + if (username) { + return { + unreadNotification: true, + user: { + __typename: 'UserDetail', + avatarTemplate: mockUsers[0].avatarTemplate, + username: mockUsers[0].username, + name: mockUsers[0].name, + websiteName: null, + bioRaw: null, + location: null, + dateOfBirth: null, + email: 'johndoe@test.com', + secondaryEmails: [], + unconfirmedEmails: [], + canEditUsername: true, + admin: true, + status: mockUsers[0].status, + }, + }; + } + }, + }, + Mutation: { + editProfile: ( + _: unknown, + { + username, + newUsername, + }: { + username: string; + editProfileInput: EditProfileInput; + newUsername: string; + uploadId: number; + }, + ) => { + if (username) { + return { + unreadNotification: true, + id: mockUsers[0].id, + avatarTemplate: mockUsers[0].avatarTemplate, + username: newUsername, + name: mockUsers[0].name, + websiteName: null, + bioRaw: null, + location: null, + dateOfBirth: null, + email: 'johndoe@test.com', + secondaryEmails: [], + unconfirmedEmails: [], + canEditUsername: true, + admin: true, + }; + } + }, + }, +}; diff --git a/frontend/e2e/apollo-mock/resolvers/queriesResolvers.ts b/frontend/e2e/apollo-mock/resolvers/queriesResolvers.ts new file mode 100644 index 00000000..23124d57 --- /dev/null +++ b/frontend/e2e/apollo-mock/resolvers/queriesResolvers.ts @@ -0,0 +1,28 @@ +import { mockTopics, mockUsers, mockToken } from '../data'; + +export const queriesResolvers = { + Query: { + about: () => { + return { + topicCount: mockTopics.length, + }; + }, + refreshToken: () => { + let { id, username, name } = mockUsers[0]; + + return { + token: mockToken, + id, + username, + name, + }; + }, + health: () => { + return { + isDiscourseReachable: true, + discourseHost: '', + discourseError: null, + }; + }, + }, +}; diff --git a/frontend/e2e/apollo-mock/resolvers/siteResolver.ts b/frontend/e2e/apollo-mock/resolvers/siteResolver.ts new file mode 100644 index 00000000..8d301639 --- /dev/null +++ b/frontend/e2e/apollo-mock/resolvers/siteResolver.ts @@ -0,0 +1,19 @@ +import { siteSetting } from '../data'; +import { ContextValue } from '../server'; + +export const siteResolvers = { + Query: { + site: (_: unknown, _arg: unknown, ctx: ContextValue) => { + if (!ctx.token) { + throw new Error('Authorization Failed'); + } + return siteSetting; + }, + pluginStatus: (_: unknown, _arg: unknown) => { + return { + appleLoginEnabled: false, + loginLinkEnabled: true, + }; + }, + }, +}; diff --git a/frontend/e2e/apollo-mock/resolvers/topicsResolvers.ts b/frontend/e2e/apollo-mock/resolvers/topicsResolvers.ts new file mode 100644 index 00000000..86611d04 --- /dev/null +++ b/frontend/e2e/apollo-mock/resolvers/topicsResolvers.ts @@ -0,0 +1,217 @@ +import { + mockTopics, + mockUsers, + mockTopicDetails, + mockPostsReplies, + mockMessageReplies, + mockMessageDetails, + mockNewTopics, + mockNewTopicWithPoll, + mockFirstPost, + mockPostWithPoll, + mockSearchTopics, +} from '../data'; +import { + TopicsSortEnum, + ReplyInput, + PosterOutputUnion, + NewTopicInput, + QuerySearchArgs, + EditPostInput, +} from '../generated/server'; + +export const topicResolvers = { + PosterOutputUnion: { + __resolveType(obj: PosterOutputUnion) { + if (obj.hasOwnProperty('userId')) { + return 'TopicPosterNewUnion'; + } + return 'SuggestionTopicPoster'; + }, + }, + Query: { + topics: ( + _: unknown, + { + categoryId, + }: { + sort: TopicsSortEnum; + categoryId: number; + page: number; + username: string; + }, + ) => { + const topics = categoryId + ? mockTopics.filter((t) => t.categoryId === categoryId) + : mockTopics; + const reversedArray = [...topics].reverse(); + + return { + users: mockUsers, + topicList: { + tags: null, + topics: reversedArray, + }, + }; + }, + topicDetail( + _: unknown, + { + topicId, + }: { + topicId: number; + postIds: Array; + postNumber: number; + includeFirstPost: boolean; + }, + ) { + let topicDetails = mockTopicDetails.find((t) => t.id === topicId); + + if (!topicDetails) { + throw new Error('Topic not found'); + } + + return topicDetails; + }, + postRaw( + _: unknown, + __: { + postId: number; + }, + ) { + return { + raw: mockPostWithPoll.raw, + markdownContent: mockPostWithPoll.markdownContent, + mentions: mockPostWithPoll.mentions, + }; + }, + search: (_: unknown, { search }: QuerySearchArgs) => { + const indexes: Array = mockSearchTopics.topics.reduce< + Array + >((acc, obj, index) => { + if (obj.title.toLowerCase().includes(search)) { + acc.push(index); + } + return acc; + }, []); + const filteredData = { + posts: mockSearchTopics.posts.filter((_, index) => + indexes.includes(index), + ), + topics: mockSearchTopics.topics.filter((_, index) => + indexes.includes(index), + ), + }; + + return filteredData; + }, + }, + Mutation: { + reply: (_: unknown, { replyInput }: { replyInput: ReplyInput }) => { + // This condition is for replying to a post. + let existingTopicDetail = mockTopicDetails.filter( + (t) => t.id === replyInput.topicId, + ); + let newPostReply = replyInput.replyToPostNumber + ? mockPostsReplies[2] + : mockPostsReplies[1]; + + if (existingTopicDetail.length === 1) { + let newPostStreams = [ + ...existingTopicDetail[0].postStream.stream, + newPostReply.id, + ]; + let newPosts = [ + ...existingTopicDetail[0].postStream.posts, + newPostReply, + ]; + existingTopicDetail[0].postStream.stream = newPostStreams; + existingTopicDetail[0].postStream.posts = newPosts; + + return { + id: newPostReply.id, + postNumber: newPostReply.postNumber, + }; + } + + // This condition is for replying to a message. + let existingMessageDetail = mockMessageDetails.filter( + (t) => t.id === replyInput.topicId, + ); + + if (existingMessageDetail.length === 1) { + const lengthPostStreamMessage = + existingMessageDetail[0].postStream.stream.length; + + let newPostStreams = [ + ...existingMessageDetail[0].postStream.stream, + mockMessageReplies[lengthPostStreamMessage].id, + ]; + let newPosts = [ + ...existingMessageDetail[0].postStream.posts, + mockMessageReplies[lengthPostStreamMessage], + ]; + existingMessageDetail[0].postStream.stream = newPostStreams; + existingMessageDetail[0].postStream.posts = newPosts; + + return { + id: mockMessageReplies[1].id, + postNumber: mockMessageReplies[1].postNumber, + userId: mockUsers[0].id, + createdAt: mockMessageReplies[1].createdAt, + raw: replyInput.raw, + }; + } + + throw new Error('Topic id not found'); + }, + newTopic: ( + _: unknown, + { newTopicInput }: { newTopicInput: NewTopicInput }, + ) => { + let newTopic = newTopicInput.raw.startsWith('[poll') + ? mockNewTopicWithPoll + : mockNewTopics; + mockTopics.push(newTopic); + + return { + topicId: newTopic.id, + }; + }, + editPost: ( + _: unknown, + { postId, postInput }: { postId: number; postInput: EditPostInput }, + ) => { + let combinedPost = [mockFirstPost, ...mockPostsReplies]; + let existingPost = combinedPost.find((t) => t.id === postId); + if (existingPost) { + existingPost.markdownContent = postInput.raw; + + return { + id: existingPost.id, + postNumber: existingPost.postNumber, + }; + } + + // This condition is for editing poll options + if (mockPostWithPoll.id === postId) { + mockPostWithPoll.markdownContent = postInput.raw; + mockPostWithPoll.polls[0].options = [ + ...mockPostWithPoll.polls[0].options, + { + id: '24v7ozi4art46j6ag4oe89u24jkc42v5', + html: 'Grape', + votes: 0, + }, + ]; + + return { + id: mockPostWithPoll.id, + postNumber: mockPostWithPoll.postNumber, + }; + } + + throw new Error('Post id not found'); + }, + }, +}; diff --git a/frontend/e2e/apollo-mock/resolvers/userActivityResolver.ts b/frontend/e2e/apollo-mock/resolvers/userActivityResolver.ts new file mode 100644 index 00000000..c8ad0ca2 --- /dev/null +++ b/frontend/e2e/apollo-mock/resolvers/userActivityResolver.ts @@ -0,0 +1,37 @@ +import { allTopics, allPosts } from '../data'; + +export const userActivityResolvers = { + Query: { + userActivity: ( + _: unknown, + { username }: { username: string; offset: number; filter: string }, + ) => { + let userActivities = allPosts.filter( + (item) => item.username === username, + ); + + if (userActivities.length > 0) { + return userActivities.map((item) => { + let topic = allTopics.find((topic) => topic.id === item.topicId); + + return { + topicId: item.topicId, + postId: item.id, + actionType: 4, + avatarTemplate: item.avatarTemplate, + createdAt: item.createdAt, + excerpt: topic?.excerpt, + categoryId: topic?.categoryId, + postNumber: item.postNumber, + markdownContent: item.markdownContent, + title: topic?.title, + username, + hidden: false, + }; + }); + } + + throw new Error('User not found'); + }, + }, +}; diff --git a/frontend/e2e/apollo-mock/resolvers/userResolvers.ts b/frontend/e2e/apollo-mock/resolvers/userResolvers.ts new file mode 100644 index 00000000..32e105c9 --- /dev/null +++ b/frontend/e2e/apollo-mock/resolvers/userResolvers.ts @@ -0,0 +1,11 @@ +import { mockUsers } from '../data'; + +export const userResolvers = { + Query: { + searchUser: (_: unknown) => { + return { + users: mockUsers, + }; + }, + }, +}; diff --git a/frontend/e2e/apollo-mock/resolvers/userStatusResolver.ts b/frontend/e2e/apollo-mock/resolvers/userStatusResolver.ts new file mode 100644 index 00000000..97b49a6f --- /dev/null +++ b/frontend/e2e/apollo-mock/resolvers/userStatusResolver.ts @@ -0,0 +1,30 @@ +import { mockUsers } from '../data'; + +export const userStatusResolvers = { + Mutation: { + editUserStatus: ( + _: unknown, + { + endsAt, + emoji, + description, + }: { endsAt?: string; emoji: string; description: string }, + ) => { + if (mockUsers[0].status) { + mockUsers[0].status = { + ...mockUsers[0].status, + ...{ emoji, endsAt, description }, + }; + } + + return 'success'; + }, + deleteUserStatus: () => { + if (mockUsers[0].status) { + mockUsers[0].status = null; + } + + return 'success'; + }, + }, +}; diff --git a/frontend/e2e/apollo-mock/server.ts b/frontend/e2e/apollo-mock/server.ts new file mode 100644 index 00000000..82a006df --- /dev/null +++ b/frontend/e2e/apollo-mock/server.ts @@ -0,0 +1,91 @@ +import fs from 'fs'; +import path from 'path'; +import http from 'http'; + +import { ApolloServer } from 'apollo-server-express'; +import { ApolloServerPluginDrainHttpServer } from 'apollo-server-core'; +import { makeExecutableSchema } from '@graphql-tools/schema'; +import express from 'express'; + +import { MOCK_SERVER_PORT } from '../global'; + +import { + loginResolvers, + topicResolvers, + categoriesResolvers, + queriesResolvers, + mutationsResolvers, + messageResolvers, + profileResolvers, + siteResolvers, + userResolvers, + pollResolvers, + userStatusResolvers, + userActivityResolvers, +} from './resolvers'; + +export type MockServerContext = { + server: ApolloServer; + stop: () => void; +}; +export type ContextValue = { + token?: string; +}; + +/** + * The mocked `schema.graphql` is automatically generated after running yarn `graphql:generate` or `yarn mock:generate`. + * Before using these two commands, you must first run `yarn generate` from the API directory. + */ + +const pathDir = path.dirname(__filename); + +const pathSchema = path.join(pathDir, 'generated', 'schema.graphql'); + +const typeSchema = fs.readFileSync(pathSchema, 'utf8'); + +export async function startMockServer(): Promise { + const schema = makeExecutableSchema({ + typeDefs: [typeSchema], + resolvers: [ + loginResolvers, + topicResolvers, + categoriesResolvers, + queriesResolvers, + mutationsResolvers, + messageResolvers, + profileResolvers, + siteResolvers, + userResolvers, + pollResolvers, + userStatusResolvers, + userActivityResolvers, + ], + }); + + const app = express(); + app.use('/images/emoji/twitter', express.static(__dirname + '/assets')); + const httpServer = http.createServer(app); + + const server = new ApolloServer({ + schema, + context: ({ req }) => { + return { token: JSON.stringify(req.headers.authorization) }; + }, + plugins: [ApolloServerPluginDrainHttpServer({ httpServer })], + }); + + await server.start(); + server.applyMiddleware({ app, path: '/' }); + + await new Promise((resolve) => + httpServer.listen({ port: MOCK_SERVER_PORT }, resolve), + ); + + return { + server, + stop: async () => { + await server.stop(); + httpServer.close(); + }, + }; +} diff --git a/frontend/e2e/global/constant.ts b/frontend/e2e/global/constant.ts new file mode 100644 index 00000000..733e9e59 --- /dev/null +++ b/frontend/e2e/global/constant.ts @@ -0,0 +1 @@ +export const MOCK_SERVER_PORT = '8929'; diff --git a/frontend/e2e/global/index.ts b/frontend/e2e/global/index.ts new file mode 100644 index 00000000..1593c6ca --- /dev/null +++ b/frontend/e2e/global/index.ts @@ -0,0 +1 @@ +export * from './constant'; diff --git a/frontend/e2e/helpers/deepLink.ts b/frontend/e2e/helpers/deepLink.ts new file mode 100644 index 00000000..02c82366 --- /dev/null +++ b/frontend/e2e/helpers/deepLink.ts @@ -0,0 +1,39 @@ +import fs from 'fs'; +import path from 'path'; + +import { device } from 'detox'; + +const appJsonPath = path.join(__dirname, '../../../frontend/app.json'); + +type RedirectArgs = { + type: 'post' | 'message' | 'login' | 'activate'; + content: string; +}; + +export async function redirectToApp({ type, content }: RedirectArgs) { + const appJson = fs.readFileSync(appJsonPath, 'utf8'); + const { expo } = JSON.parse(appJson); + const scheme = expo.scheme; + + let link = `${scheme}://`; + + switch (type) { + case 'post': + link += `post-detail/${content}`; + break; + case 'message': + link += `message-detail/${content}`; + break; + case 'activate': + link += `activate-account/${content}`; + break; + case 'login': + link += `email-login/${content}`; + break; + } + + await device.launchApp({ + newInstance: false, + url: link, + }); +} diff --git a/frontend/e2e/helpers/index.ts b/frontend/e2e/helpers/index.ts new file mode 100644 index 00000000..c8e1a6cd --- /dev/null +++ b/frontend/e2e/helpers/index.ts @@ -0,0 +1,6 @@ +export * from './login'; +export * from './logout'; +export * from './tab'; +export * from './post'; +export * from './link'; +export * from './deepLink'; diff --git a/frontend/e2e/helpers/link.ts b/frontend/e2e/helpers/link.ts new file mode 100644 index 00000000..87a234e5 --- /dev/null +++ b/frontend/e2e/helpers/link.ts @@ -0,0 +1,16 @@ +import { by, expect, element } from 'detox'; + +export async function hyperLinkScene() { + await expect(element(by.text('Insert Hyperlink')).atIndex(0)).toBeVisible(); + + const textInputUrl = element(by.id('Hyperlink:TextInput:URL')); + await expect(textInputUrl).toBeVisible(); + await textInputUrl.replaceText('www.google.com'); + + const textInputTitle = element(by.id('Hyperlink:TextInput:Title')); + await textInputTitle.replaceText('test url'); + + const textDone = device.getPlatform() === 'android' ? 'Add' : 'Done'; + await expect(element(by.text(textDone)).atIndex(0)).toBeVisible(); + await element(by.text(textDone)).atIndex(0).tap(); +} diff --git a/frontend/e2e/helpers/login.ts b/frontend/e2e/helpers/login.ts new file mode 100644 index 00000000..54ec7ceb --- /dev/null +++ b/frontend/e2e/helpers/login.ts @@ -0,0 +1,26 @@ +import { by, expect, element, waitFor } from 'detox'; + +export async function loginWithAccount() { + await waitFor(element(by.id('Login:TextInput:Email'))) + .toBeVisible() + .withTimeout(9000); + await expect(element(by.id('Login:TextInput:Email'))).toBeVisible(); + await element(by.id('Login:TextInput:Email')).tap(); + await element(by.id('Login:TextInput:Email')).typeText('test@example.com\n'); + await expect(element(by.id('Login:TextInput:Email'))).toHaveText( + 'test@example.com', + ); + + await expect(element(by.id('Login:TextInput:Password'))).toBeVisible(); + await element(by.id('Login:TextInput:Password')).typeText('password\n'); + await expect(element(by.id('Login:TextInput:Password'))).toHaveText( + 'password', + ); + + await waitFor(element(by.id('Login:Button:Login'))) + .toBeVisible() + .withTimeout(1000); + await element(by.id('Login:Button:Login')).tap(); + + await expect(element(by.text('Home'))).toBeVisible(); +} diff --git a/frontend/e2e/helpers/logout.ts b/frontend/e2e/helpers/logout.ts new file mode 100644 index 00000000..52699b83 --- /dev/null +++ b/frontend/e2e/helpers/logout.ts @@ -0,0 +1,12 @@ +import { by, element } from 'detox'; + +import { waitTabProfile } from './tab'; + +export async function logout() { + await waitTabProfile(); + + await element(by.id('Tab:Profile')).tap(); + + await element(by.id('Profile:ScrollView')).scrollTo('bottom'); + await element(by.id('Profile:MenuItem:Logout')).tap(); +} diff --git a/frontend/e2e/helpers/post.ts b/frontend/e2e/helpers/post.ts new file mode 100644 index 00000000..d2838df2 --- /dev/null +++ b/frontend/e2e/helpers/post.ts @@ -0,0 +1,25 @@ +import { by, expect, element, device } from 'detox'; + +export async function createPost() { + await expect(element(by.text('Next'))).toBeVisible(); + await element(by.text('Next')).tap(); + + let postButtonTestId = ''; + if (device.getPlatform() === 'android') { + postButtonTestId = 'HeaderItem:IconOnly'; + } else { + postButtonTestId = 'HeaderItem:LabelOnly'; + } + await expect(element(by.id(postButtonTestId))).toBeVisible(); + await element(by.id(postButtonTestId)).tap(); +} + +export async function redirectPostDetail() { + const post = element(by.text('Detox Test')).atIndex(0); + await waitFor(post) + .toBeVisible() + .whileElement(by.id('Home:PostList')) + .scroll(150, 'down'); + await post.tap(); + await expect(element(by.id('PostDetail:List'))).toBeVisible(); +} diff --git a/frontend/e2e/helpers/tab.ts b/frontend/e2e/helpers/tab.ts new file mode 100644 index 00000000..7dd8ec98 --- /dev/null +++ b/frontend/e2e/helpers/tab.ts @@ -0,0 +1,7 @@ +import { by, element, waitFor } from 'detox'; + +export async function waitTabProfile() { + await waitFor(element(by.id('Tab:Profile'))) + .toBeVisible() + .withTimeout(5000); +} diff --git a/frontend/e2e/init.ts b/frontend/e2e/init.ts new file mode 100644 index 00000000..3beb007d --- /dev/null +++ b/frontend/e2e/init.ts @@ -0,0 +1,23 @@ +import { device } from 'detox'; +import { config } from 'detox/internals'; + +import { MockServerContext, startMockServer } from './apollo-mock/server'; + +let mockServer: MockServerContext; +// Set the default timeout +const DEFAULT_TIMEOUT_MS = 10 * 60 * 1000; +jest.setTimeout(DEFAULT_TIMEOUT_MS); + +beforeAll(async () => { + mockServer = await startMockServer(); + + if (config.behavior.init?.reinstallApp === false) { + await device.installApp(); + } + + return device.launchApp(); +}); + +afterAll(() => { + mockServer.stop(); +}); diff --git a/frontend/e2e/jest.config.js b/frontend/e2e/jest.config.js new file mode 100644 index 00000000..e1c05346 --- /dev/null +++ b/frontend/e2e/jest.config.js @@ -0,0 +1,13 @@ +/** @type {import('@jest/types').Config.InitialOptions} */ +module.exports = { + rootDir: '..', + testRegex: '\\.e2e\\.ts$', + testTimeout: 120000, + maxWorkers: 1, + globalSetup: 'detox/runners/jest/globalSetup', + globalTeardown: 'detox/runners/jest/globalTeardown', + reporters: ['detox/runners/jest/reporter'], + testEnvironment: 'detox/runners/jest/testEnvironment', + setupFilesAfterEnv: ['/e2e/init.ts'], + verbose: true, +}; diff --git a/frontend/e2e/tests/activity.e2e.ts b/frontend/e2e/tests/activity.e2e.ts new file mode 100644 index 00000000..a62cbc89 --- /dev/null +++ b/frontend/e2e/tests/activity.e2e.ts @@ -0,0 +1,17 @@ +import { by, expect, element } from 'detox'; + +import { waitTabProfile } from '../helpers'; + +describe('Activity', () => { + it("should correctly show the user's activity", async () => { + await waitTabProfile(); + await expect(element(by.id('Home:PostList'))).toBeVisible(); + + await expect(element(by.id('Author:Avatar')).atIndex(0)).toBeVisible(); + await element(by.id('Author:Avatar')).atIndex(0).tap(); + + await expect(element(by.id('UserInformation:PostList'))).toBeVisible(); + await element(by.text('Detox Test')).atIndex(0).tap(); + await expect(element(by.id('PostDetail:List'))).toBeVisible(); + }); +}); diff --git a/frontend/e2e/tests/deepLink.e2e.ts b/frontend/e2e/tests/deepLink.e2e.ts new file mode 100644 index 00000000..d3c2d33b --- /dev/null +++ b/frontend/e2e/tests/deepLink.e2e.ts @@ -0,0 +1,59 @@ +import { device, by, expect, element } from 'detox'; + +import { logout, redirectToApp } from '../helpers'; +import { emailToken } from '../apollo-mock/data'; + +const openNewInstanceLogin = async () => { + await device.launchApp({ + newInstance: true, + }); + await logout(); +}; + +describe('Deep link', () => { + it('should handle URL successfully into post detail', async () => { + await redirectToApp({ type: 'post', content: 't/Detox-test/3' }); + + await expect(element(by.id('PostDetail:List'))).toBeVisible(); + await expect(element(by.text('Detox Test')).atIndex(0)).toBeVisible(); + }); + + it('should handle URL successfully into message detail', async () => { + await redirectToApp({ + type: 'message', + content: 't/Testing-new-message/6', + }); + await expect(element(by.id('ReplyInputField:TextInput'))).toBeVisible(); + await expect( + element(by.text('Testing new message')).atIndex(0), + ).toBeVisible(); + }); + + it('should show error not valid link', async () => { + await openNewInstanceLogin(); + + await redirectToApp({ type: 'login', content: 'random-token' }); + + await expect(element(by.text('Sorry link is not valid'))).toBeVisible(); + await element(by.text('Got it')).tap(); + }); + + it('should handle URL successfully login link', async () => { + await redirectToApp({ type: 'login', content: emailToken }); + + await expect(element(by.text('Home'))).toBeVisible(); + }); + + it('should handle URL successfully activate account and login into home', async () => { + await openNewInstanceLogin(); + await redirectToApp({ type: 'activate', content: emailToken }); + + await expect( + element(by.text('Your new account is confirmed')), + ).toBeVisible(); + const gotItButton = element(by.text('Got it')); + await expect(gotItButton).toBeVisible(); + await gotItButton.tap(); + await expect(element(by.text('Home'))).toBeVisible(); + }); +}); diff --git a/frontend/e2e/tests/login.e2e.ts b/frontend/e2e/tests/login.e2e.ts new file mode 100644 index 00000000..90cf5b68 --- /dev/null +++ b/frontend/e2e/tests/login.e2e.ts @@ -0,0 +1,45 @@ +import { by, expect, element, waitFor } from 'detox'; + +import { loginWithAccount, logout } from '../helpers'; + +describe('Login Screen', () => { + it('should have error in login screen', async () => { + await logout(); + + await expect(element(by.text('Log In')).atIndex(0)).toBeVisible(); + await expect(element(by.text('Log In')).atIndex(1)).toBeVisible(); + await expect(element(by.id('Login:Button:Login'))).toBeVisible(); + + await expect(element(by.id('Login:TextInput:Email'))).toBeVisible(); + await element(by.id('Login:TextInput:Email')).typeText( + 'wrong@example.com\n', + ); + await expect(element(by.id('Login:TextInput:Email'))).toHaveText( + 'wrong@example.com', + ); + + await expect(element(by.id('Login:TextInput:Password'))).toBeVisible(); + await element(by.id('Login:TextInput:Password')).typeText( + 'wrong password\n', + ); + await expect(element(by.id('Login:TextInput:Password'))).toHaveText( + 'wrong password', + ); + + await waitFor(element(by.id('Login:Button:Login'))) + .toBeVisible() + .withTimeout(1000); + await element(by.id('Login:Button:Login')).tap(); + + await expect( + element(by.text('Incorrect username, email or password')), + ).toBeVisible(); + }); + + it('should login with account', async () => { + // clear the text input without using reload + await element(by.id('Login:TextInput:Email')).clearText(); + await element(by.id('Login:TextInput:Password')).clearText(); + await loginWithAccount(); + }); +}); diff --git a/frontend/e2e/tests/messages.e2e.ts b/frontend/e2e/tests/messages.e2e.ts new file mode 100644 index 00000000..cbbaf484 --- /dev/null +++ b/frontend/e2e/tests/messages.e2e.ts @@ -0,0 +1,133 @@ +import { by, expect, element, waitFor } from 'detox'; + +import { hyperLinkScene, waitTabProfile } from '../helpers'; +import { mockMessageReplies, mockNewMessageTopic } from '../apollo-mock/data'; + +const redirectToMessageDetail = async () => { + const message = element(by.text('Testing new message')).atIndex(0); + await message.tap(); +}; + +const linkButtonBarMessage = async () => { + const buttonToolTip = element(by.id('ToolTip:AddIcon')); + await expect(buttonToolTip).toBeVisible(); + await buttonToolTip.tap(); + + const buttonAddLink = element(by.text('Add Link')).atIndex(0); + await expect(buttonAddLink).toBeVisible(); + await buttonAddLink.tap(); + + await hyperLinkScene(); +}; + +describe('Message Screen', () => { + it('should show message list in Messages screen', async () => { + await waitTabProfile(); + await element(by.id('Tab:Profile')).tap(); + + await expect(element(by.id('Profile:MenuItem:Messages'))).toBeVisible(); + await element(by.id('Profile:MenuItem:Messages')).tap(); + + await expect(element(by.id('Messages:List'))).toBeVisible(); + await redirectToMessageDetail(); + }); + + it('should reply to a message', async () => { + const replyMessage = mockMessageReplies[1].markdownContent; + await expect(element(by.id('ReplyInputField:TextInput'))).toBeVisible(); + await element(by.id('ReplyInputField:TextInput')).typeText(replyMessage); + + await expect(element(by.id('ReplyInputField:Icon:Reply'))).toBeVisible(); + await element(by.id('ReplyInputField:Icon:Reply')).tap(); + await expect(element(by.text(replyMessage)).atIndex(0)).toBeVisible(); + }); + + it('should reply to a message With Link', async () => { + await expect(element(by.id('ReplyInputField:TextInput'))).toBeVisible(); + + await linkButtonBarMessage(); + + await expect(element(by.id('ReplyInputField:TextInput'))).toHaveText( + mockMessageReplies[2].markdownContent, + ); + + await expect(element(by.id('ReplyInputField:Icon:Reply'))).toBeVisible(); + await element(by.id('ReplyInputField:Icon:Reply')).tap(); + await expect( + element(by.text('https://www.google.com')).atIndex(0), + ).toBeVisible(); + + await element(by.id('HeaderBackButton')).tap(); + }); + + it('should create a new message', async () => { + if (device.getPlatform() === 'android') { + await expect(element(by.id('FloatingButton'))).toBeVisible(); + await element(by.id('FloatingButton')).tap(); + } else { + await expect(element(by.id('HeaderItem:IconOnly'))).toBeVisible(); + await element(by.id('HeaderItem:IconOnly')).tap(); + } + + await expect(element(by.id('NewMessage:TextArea'))).toBeVisible(); + await element(by.id('NewMessage:TextArea')).typeText( + mockNewMessageTopic.excerpt, + ); + + await expect(element(by.id('NewMessage:TextInput:Title'))).toBeVisible(); + await element(by.id('NewMessage:TextInput:Title')).typeText( + mockNewMessageTopic.title, + ); + + await element(by.id('NewMessage:Button:SelectUser')).tap(); + await expect(element(by.id('SelectUser:SafeAreaView'))).toBeVisible(); + + await expect( + element(by.id('UserItem:Author:SelectUser')).atIndex(0), + ).toBeVisible(); + await element(by.id('UserItem:Author:SelectUser')).atIndex(0).tap(); + await element(by.text('Done')).tap(); + + await expect(element(by.text('Send')).atIndex(0)).toBeVisible(); + await element(by.text('Send')).atIndex(0).tap(); + + await expect(element(by.id('Messages:List'))).toBeVisible(); + await expect( + element(by.text(mockNewMessageTopic.title)).atIndex(0), + ).toBeVisible(); + }); + + it('should leave Messages screen', async () => { + await expect(element(by.id('Messages:List'))).toBeVisible(); + + await redirectToMessageDetail(); + + const actionSheet = element(by.id('HeaderItem:IconOnly')); + await expect(actionSheet).toBeVisible(); + await actionSheet.tap(); + + const leaveActionSheetOption = element(by.text('Leave Message')).atIndex(0); + await expect(leaveActionSheetOption).toBeVisible(); + await leaveActionSheetOption.tap(); + + /** + * Should show pop up to confirmation leave private message + */ + + const leaveAlert = element( + by.text(device.getPlatform() === 'android' ? 'LEAVE' : 'Leave'), + ).atIndex(0); + await expect(leaveAlert).toBeVisible(); + await leaveAlert.tap(); + + /** + * It will redirect to the messageList scene, where the leave message will be removed from the list. + */ + + await waitFor(element(by.id('Messages:List'))) + .toBeVisible() + .withTimeout(2000); + + await expect(element(by.id('MessageList:MessageCard:6'))).not.toBeVisible(); + }); +}); diff --git a/frontend/e2e/tests/polls.e2e.ts b/frontend/e2e/tests/polls.e2e.ts new file mode 100644 index 00000000..f741e277 --- /dev/null +++ b/frontend/e2e/tests/polls.e2e.ts @@ -0,0 +1,110 @@ +import { by, expect, element, device } from 'detox'; + +import { createPost, waitTabProfile } from '../helpers'; +import { mockNewTopicWithPoll } from '../apollo-mock/data'; + +async function createPoll() { + await expect(element(by.id('NewPoll:SafeAreaView'))).toBeVisible(); + await element(by.id('NewPoll:TextInput:Options')) + .atIndex(0) + .replaceText('Apple'); + await element(by.id('NewPoll:Button:AddOption')).tap(); + await element(by.id('NewPoll:TextInput:Options')) + .atIndex(1) + .replaceText('Banana'); + await element(by.id('NewPoll:Button:AddOption')).tap(); + await element(by.id('NewPoll:TextInput:Options')) + .atIndex(2) + .replaceText('Mango\n'); + + await element(by.id('NewPoll:Button:AdvancedSettings')).tap(); + await element(by.id('NewPoll:ScrollView')).scrollTo('bottom'); + + await expect(element(by.id('NewPoll:TextInput:Title'))).toBeVisible(); + await element(by.id('NewPoll:TextInput:Title')).replaceText('Favorite Fruit'); + await element(by.text('Done')).tap(); +} + +describe('Polls', () => { + it('should create a new post with poll', async () => { + await waitTabProfile(); + await expect(element(by.id('Home:PostList'))).toBeVisible(); + + if (device.getPlatform() === 'android') { + await expect(element(by.id('FloatingButton'))).toBeVisible(); + await element(by.id('FloatingButton')).tap(); + } else { + await expect(element(by.id('HomeNavBar:Icon:Add'))).toBeVisible(); + await element(by.id('HomeNavBar:Icon:Add')).tap(); + } + + await expect(element(by.id('NewPost:TextInput:Title'))).toBeVisible(); + await element(by.id('NewPost:TextInput:Title')).typeText( + mockNewTopicWithPoll.title, + ); + + await expect(element(by.id('NewPost:Button:Channel'))).toBeVisible(); + await element(by.id('NewPost:Button:Channel')).tap(); + await element(by.text('Lexicon UAT')).atIndex(0).tap(); + + await expect(element(by.id('NewPost:TextArea'))).toBeVisible(); + await element(by.id('NewPost:TextArea')).tap(); + + if (device.getPlatform() === 'android') { + await element(by.id('BottomMenu:ScrollView')).scrollToIndex(7); + } else { + await element(by.id('BottomMenu:ScrollView')).scrollTo('right'); + } + await element(by.id('BottomMenu:IconPoll')).atIndex(0).tap(); + + await createPoll(); + await expect(element(by.id('PollChoiceCard:View'))).toBeVisible(); + + await createPost(); + await expect(element(by.id('PostDetail:List'))).toBeVisible(); + }); + + it('should be able to interact with poll', async () => { + await expect(element(by.id('PollPreview:View'))).toBeVisible(); + + // Vote poll + await element(by.text('Banana')).tap(); + await expect(element(by.id('StackedAvatars:Button'))).toBeVisible(); + await expect(element(by.text('100%'))).toBeVisible(); + await expect(element(by.text('Total: 1 voter'))).toBeVisible(); + + // Undo vote poll + await expect(element(by.text('Undo Vote'))).toBeVisible(); + await element(by.text('Undo Vote')).tap(); + await expect(element(by.text('100%'))).not.toBeVisible(); + await expect(element(by.id('StackedAvatars:Button'))).not.toBeVisible(); + await expect(element(by.text('Total: 0 voter'))).toBeVisible(); + + // Close poll + await expect(element(by.text('Close Poll'))).toBeVisible(); + await element(by.text('Close Poll')).tap(); + await expect(element(by.text('Open Poll'))).toBeVisible(); + await element(by.text('Open Poll')).tap(); + }); + + it('should be able to edit poll', async () => { + await element(by.id('HeaderItem:IconOnly')).tap(); + await element(by.text('Edit Post')).tap(); + await expect(element(by.id('NewPost:SafeAreaView'))).toBeVisible(); + + await element(by.id('PollChoiceCard:Icon:Edit')).tap(); + await expect(element(by.id('NewPoll:SafeAreaView'))).toBeVisible(); + + await element(by.id('NewPoll:Button:AddOption')).tap(); + await element(by.id('NewPoll:TextInput:Options')) + .atIndex(3) + .replaceText('Grape'); + await element(by.text('Done')).tap(); + await expect(element(by.id('PollChoiceCard:View'))).toBeVisible(); + + await createPost(); + await expect(element(by.id('PostDetail:List'))).toBeVisible(); + + await expect(element(by.text('Grape'))).toBeVisible(); + }); +}); diff --git a/frontend/e2e/tests/profile.e2e.ts b/frontend/e2e/tests/profile.e2e.ts new file mode 100644 index 00000000..31b0f0ba --- /dev/null +++ b/frontend/e2e/tests/profile.e2e.ts @@ -0,0 +1,24 @@ +import { by, expect, element } from 'detox'; + +import { waitTabProfile } from '../helpers'; + +describe('Profile Tab', () => { + it('should successfully change username', async () => { + await waitTabProfile(); + await element(by.id('Tab:Profile')).tap(); + + await element(by.id('Profile:Button:EditProfile')).tap(); + await expect(element(by.id('EditProlfie:ScrollView'))).toBeVisible(); + + const usernameTextInput = element(by.id('EditProfile:TextInput:Username')); + await expect(usernameTextInput).toBeVisible(); + await expect(usernameTextInput).toHaveText('johndoe'); + await usernameTextInput.typeText('19'); + await expect(usernameTextInput).toHaveText('johndoe19'); + + await element(by.text('Save')).atIndex(0).tap(); + + await expect(element(by.text('Got it')).atIndex(0)).toBeVisible(); + await element(by.text('Got it')).atIndex(0).tap(); + }); +}); diff --git a/frontend/e2e/tests/topics.e2e.ts b/frontend/e2e/tests/topics.e2e.ts new file mode 100644 index 00000000..c8322b51 --- /dev/null +++ b/frontend/e2e/tests/topics.e2e.ts @@ -0,0 +1,193 @@ +import { by, expect, element } from 'detox'; + +import { + createPost, + waitTabProfile, + redirectPostDetail, + hyperLinkScene, +} from '../helpers'; +import { + mockSearchTopics, + siteSetting, + mockPostsReplies, +} from '../apollo-mock/data'; + +async function fontFormattingTest( + postReplyTextArea: Detox.IndexableNativeElement, +) { + await postReplyTextArea.typeText('Test Bold'); + + const buttonBold = element(by.id('BottomMenu:IconBold')); + await expect(buttonBold).toBeVisible(); + await buttonBold.tap(); + + await expect(postReplyTextArea).toHaveText(`Test Bold**Change Text**`); + + await postReplyTextArea.clearText(); + + const buttonItalic = element(by.id('BottomMenu:IconItalic')); + + await expect(buttonItalic).toBeVisible(); + await buttonItalic.tap(); + + await expect(postReplyTextArea).toHaveText(`*Change Text*`); + + await postReplyTextArea.clearText(); + await postReplyTextArea.typeText('Test Quote'); + const buttonQuote = element(by.id('BottomMenu:IconQuote')); + + await expect(buttonQuote).toBeVisible(); + await buttonQuote.tap(); + + await expect(postReplyTextArea).toHaveText(`Test Quote\n\n> Quote Text`); + + await postReplyTextArea.clearText(); + await postReplyTextArea.typeText('Test Bullet List'); + + const buttonBullet = element(by.id('BottomMenu:IconBulletList')); + + await expect(buttonBullet).toBeVisible(); + await buttonBullet.tap(); + + await expect(postReplyTextArea).toHaveText(`Test Bullet List\n\n- List Item`); + + await postReplyTextArea.clearText(); + await postReplyTextArea.typeText('Test Number List'); + const buttonNumber = element(by.id('BottomMenu:IconNumberList')); + + await expect(buttonNumber).toBeVisible(); + await buttonNumber.tap(); + + await expect(postReplyTextArea).toHaveText( + `Test Number List\n\n1. List Item`, + ); +} + +const linkButtonBar = async () => { + if (device.getPlatform() === 'android') { + await element(by.id('BottomMenu:ScrollView')).scrollToIndex(6); + } else { + await element(by.id('BottomMenu:ScrollView')).scrollTo('right'); + } + await element(by.id('BottomMenu:Link')).tap(); + + await hyperLinkScene(); +}; + +describe('Home Screen', () => { + it('should show Search Topic', async () => { + await waitTabProfile(); + await expect(element(by.id('Home:PostList'))).toBeVisible(); + await element(by.id('Home:Button:SearchTopic')).tap(); + + /** + * Search Scene + */ + const searchTextInput = element(by.id('Search:TextInput:Query')); + await expect(searchTextInput).toBeVisible(); + + await searchTextInput.replaceText('welcome'); + + await expect(element(by.id('Search:SearchPostList'))).toBeVisible(); + await expect( + element(by.id(`Search:SearchPostItem:${mockSearchTopics.topics[0].id}`)), + ).toBeVisible(); + + const searchDeleteButton = element(by.id('Search:Button:Delete')); + await searchDeleteButton.tap(); + await expect(searchTextInput).toHaveText(''); + + /** + * Start new search with error + */ + + await searchTextInput.typeText('a'); + + await expect( + element( + by.text( + `Your query must be at least ${siteSetting.minSearchLength} characters long`, + ), + ).atIndex(0), + ).toBeVisible(); + + await searchTextInput.replaceText('why'); + await expect( + element(by.text(`No results found for `)).atIndex(0), + ).toBeVisible(); + + /** + * Back into Home scene + */ + const backButton = element( + by.id( + device.getPlatform() === 'android' + ? 'Search:Button:HeaderBackButton' + : 'Search:Button:Delete', + ), + ); + await expect(backButton).toBeVisible(); + await backButton.tap(); + }); + + it('should show post list in Home screen', async () => { + await expect(element(by.id('Home:PostList'))).toBeVisible(); + await element(by.id('HomeNavBar:Button:SelectChannel')).tap(); + await element(by.text('Lexicon UAT')).atIndex(0).tap(); + await expect(element(by.id('Home:PostList'))).toBeVisible(); + + await redirectPostDetail(); + }); + + it('should reply to a post with font formatting test and link', async () => { + await expect(element(by.id('PostDetail:Button:Reply'))).toBeVisible(); + await element(by.id('PostDetail:Button:Reply')).tap(); + + const replyText = 'Sending a reply.'; + const postReplyTextArea = element(by.id('PostReply:TextArea')); + await expect(postReplyTextArea).toBeVisible(); + + await fontFormattingTest(postReplyTextArea); + + await postReplyTextArea.clearText(); + await postReplyTextArea.replaceText(replyText); + await linkButtonBar(); + + await expect(postReplyTextArea).toBeVisible(); + + await expect(postReplyTextArea).toHaveText( + `${replyText} [test url](https://www.google.com)`, + ); + + await createPost(); + + await expect(element(by.id('PostDetail:List'))).toBeVisible(); + }); + + it('should edit a post', async () => { + await element(by.id('HeaderItem:IconOnly')).tap(); + await element(by.text('Edit Post')).tap(); + + const postReplyTextArea = element(by.id('NewPost:TextArea')); + await postReplyTextArea.typeText('. Edit this reply.'); + + await createPost(); + + await expect(element(by.id('PostDetail:List'))).toBeVisible(); + }); + + it('should quote reply to a comment', async () => { + await element(by.id('Metrics:Replies')).atIndex(1).tap(); + + const postReplyTextArea = element(by.id('PostReply:TextArea')); + const repliedText = mockPostsReplies[2].markdownContent; + await postReplyTextArea.replaceText(repliedText); + + await createPost(); + + await expect(element(by.id('PostDetail:List'))).toBeVisible(); + + await element(by.id('PostDetail:List')).scrollTo('bottom'); + await expect(element(by.text(repliedText))).toBeVisible(); + }); +}); diff --git a/frontend/e2e/tests/userStatus.e2e.ts b/frontend/e2e/tests/userStatus.e2e.ts new file mode 100644 index 00000000..c7e8975b --- /dev/null +++ b/frontend/e2e/tests/userStatus.e2e.ts @@ -0,0 +1,123 @@ +import { by, expect, element } from 'detox'; + +import { redirectPostDetail, waitTabProfile } from '../helpers'; + +describe('User Status', () => { + it('should Show status in Post Detail', async () => { + await waitTabProfile(); + await expect(element(by.id('Home:PostList'))).toBeVisible(); + + await redirectPostDetail(); + + const emojiUserStatusHeader = element( + by.id('PostDetailHeaderItem:Author:EmojiStatus'), + ); + + await expect(emojiUserStatusHeader).toBeVisible(); + + const emojiUserStatusComment = element( + by.id('PostDetail:NestedComment:Author:EmojiStatus'), + ).atIndex(0); + await expect(emojiUserStatusComment).toBeVisible(); + await element(by.id('HeaderBackButton')).tap(); + }); + + it('should Show status in Profile', async () => { + await waitTabProfile(); + await element(by.id('Tab:Profile')).tap(); + + const userStatusElement = element(by.id('Profile:UserStatus')); + await expect(userStatusElement).toBeVisible(); + await expect(userStatusElement).toHaveLabel('Test Status'); + + const userStatusEmoji = element(by.id('UserStatus:Emoji:Image')); + await expect(userStatusEmoji).toBeVisible(); + }); + + it('should edit status', async () => { + const userStatusElement = element(by.id('Profile:UserStatus')); + await expect(userStatusElement).toBeVisible(); + + await userStatusElement.tap(); + + /** + * Edit status scene + */ + + const textInputStatusElement = element( + by.id('EditUserStatus:TextInput:Status'), + ); + await expect(textInputStatusElement).toBeVisible(); + + const newStatus = 'Change Status'; + + await textInputStatusElement.replaceText(newStatus); + await expect(textInputStatusElement).toHaveText(newStatus); + + const buttonEmojiElement = element(by.id('EditUserStatus:Button:Emoji')); + + await buttonEmojiElement.tap(); + + /** + * Emoji Picker Scene + */ + + const textInputSearchEmojiElement = element( + by.id('EmojiPicker:TextInput:Search'), + ); + + await expect(textInputSearchEmojiElement).toBeVisible(); + await textInputSearchEmojiElement.typeText('smile'); + + const smileEmojiElement = element(by.id('EmojiPicker:Button:Emoji:smile')); + await waitFor(smileEmojiElement).toBeVisible().withTimeout(3000); + + await smileEmojiElement.tap(); + + /** + * Back to Edit User Status scene after select emoji + */ + + await expect(textInputStatusElement).toBeVisible(); + + await expect(element(by.text('Done')).atIndex(0)).toBeVisible(); + await element(by.text('Done')).atIndex(0).tap(); + + /** + * Profile scene after edit status + */ + + await expect(userStatusElement).toBeVisible(); + await expect(userStatusElement).toHaveLabel(newStatus); + }); + + it('should delete user status', async () => { + const userStatusElement = element(by.id('Profile:UserStatus')); + await userStatusElement.tap(); + + const buttonUserStatusDelete = element( + by.id('EditUserStatus:Button:DeleteStatus'), + ); + await expect(buttonUserStatusDelete).toBeVisible(); + await buttonUserStatusDelete.tap(); + + const alertDelete = element(by.text('Delete Status?')); + + await expect(alertDelete.atIndex(0)).toBeVisible(); + + let deleteButtonAlert; + if (device.getPlatform() === 'android') { + deleteButtonAlert = 'DELETE'; + } else { + deleteButtonAlert = 'Delete'; + } + + await expect(element(by.text(deleteButtonAlert)).atIndex(0)).toBeVisible(); + await element(by.text(deleteButtonAlert)).atIndex(0).tap(); + + await expect(userStatusElement).not.toBeVisible(); + + const userStatusIconElement = element(by.id('Profile:IconWithLabel')); + await expect(userStatusIconElement).toBeVisible(); + }); +}); diff --git a/frontend/eas.json b/frontend/eas.json index 42f2c146..b22b1c64 100644 --- a/frontend/eas.json +++ b/frontend/eas.json @@ -4,10 +4,32 @@ "appVersionSource": "local" }, "build": { + "base": { + /** + * We need to add an image because starting from April 29, 2024, all apps require iOS 17 SDK, which is included in Xcode 15. + * By default, Expo 49 will use Xcode 14.3. For more information, check https://expo.dev/changelog/2023/09-28-new-xcode-ios#added-xcode-15-image-on-eas-build + * + * This image config can be deleted after updating to Expo 50. + */ + "ios": { + "image":"macos-ventura-13.6-xcode-15.0" + }, + "test": { + "channel": "test", + "android": { + "gradleCommand": ":app:assembleRelease :app:assembleAndroidTest -DtestBuildType=release", + "withoutCredentials": true + }, + "ios": { + "simulator": true + }, + "extends": "base" + }, "development": { "developmentClient": true, "distribution": "internal", - "channel": "development" + "channel": "development", + "extends": "base" }, "preview": { "distribution": "internal", @@ -17,11 +39,13 @@ }, "android": { "buildType": "apk" - } + }, + "extends": "base" }, "production": { "autoIncrement": true, - "channel": "production" + "channel": "production", + "extends": "base" } }, "submit": { diff --git a/frontend/metro.config.js b/frontend/metro.config.js index 70b5bbe1..99ea38da 100644 --- a/frontend/metro.config.js +++ b/frontend/metro.config.js @@ -1,5 +1,8 @@ -// eslint-disable-next-line @typescript-eslint/no-var-requires +/* eslint-disable @typescript-eslint/no-var-requires */ + const { getDefaultConfig } = require('expo/metro-config'); +const defaultSourceExts = + require('metro-config/src/defaults/defaults').sourceExts; const defaultConfig = getDefaultConfig(__dirname); const { @@ -12,5 +15,10 @@ defaultConfig.transformer = { babelTransformerPath: require.resolve('react-native-svg-transformer'), }; defaultConfig.resolver.assetExts = assetExts.filter((ext) => ext !== 'svg'); -defaultConfig.resolver.sourceExts.push('svg'); +(defaultConfig.resolver.sourceExts = + process.env.DETOX_TESTS === 'true' + ? ['mock.ts', 'mock.tsx', ...defaultSourceExts] + : defaultSourceExts), + defaultConfig.resolver.sourceExts.push('svg'); + module.exports = defaultConfig; diff --git a/frontend/package.json b/frontend/package.json index c8450f7b..3749d29c 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -9,14 +9,23 @@ "jestTest": "jest --coverage --passWithNoTests", "test": "yarn graphql:generate && yarn lint && yarn typecheck && yarn format:check && yarn jestTest", "eject": "expo eject", - "graphql:generate": "graphql-codegen --config codegen.ts", + "graphql:generate": "graphql-codegen --config codegen.ts && yarn mock:generate", + "mock:generate": "cp ../api/src/generated/schema.graphql e2e/apollo-mock/generated/schema.graphql", "postinstall": "patch-package", "eas-build-post-install": "yarn --cwd ../ && yarn --cwd ../ generate", "android:push:encrypt": "openssl enc -aes-256-cbc -salt -md sha512 -pbkdf2 -in ./google-services.json -out ./google-services.json.enc -k", - "dec-google-services": "openssl enc -aes-256-cbc -d -md sha512 -pbkdf2 -in ./google-services.json.enc -out ./google-services.json -k" + "dec-google-services": "openssl enc -aes-256-cbc -d -md sha512 -pbkdf2 -in ./google-services.json.enc -out ./google-services.json -k", + "tests:android:build": "detox build -c android.emu.debug", + "tests:android:test": "yarn graphql:generate && detox test -c android.emu.debug --headless --record-logs all --loglevel verbose --record-videos all --take-screenshots failing", + "start:android:test": "DETOX_TESTS=true expo start --dev-client", + "tests:ios:build": "yarn detox build --configuration ios.sim.debug", + "tests:ios:pod:install": "cd ios && rm -rf insertyourappname.xcworkspace && rm -f Podfile.lock && pod install --repo-update && cd ..", + "tests:ios:test": "yarn graphql:generate && detox test --configuration ios.sim.debug --cleanup --record-logs all --loglevel verbose --record-videos failing", + "android": "expo run:android", + "ios": "expo run:ios" }, "dependencies": { - "@apollo/client": "^3.7.1", + "@apollo/client": "^3.7.0", "@expo-google-fonts/courier-prime": "^0.2.2", "@react-native-async-storage/async-storage": "1.18.2", "@react-native-community/datetimepicker": "7.2.0", @@ -25,14 +34,15 @@ "@react-navigation/elements": "^1.3.4", "@react-navigation/native": "^6.0.11", "@react-navigation/stack": "^6.2.2", - "@shopify/flash-list": "^1.6.1", + "@shopify/flash-list": "1.4.3", "apollo-upload-client": "^17.0.0", "date-fns": "^2.30.0", "expo": "^49.0.6", + "expo-apple-authentication": "^6.1.2", "expo-constants": "~14.4.2", "expo-crypto": "~12.4.1", "expo-device": "^5.4.0", - "expo-file-system": "~15.4.4", + "expo-file-system": "~15.4.5", "expo-image-picker": "~14.3.2", "expo-linear-gradient": "~12.3.0", "expo-linking": "~5.0.2", @@ -43,14 +53,13 @@ "expo-splash-screen": "~0.20.5", "expo-status-bar": "~1.6.0", "expo-system-ui": "~2.4.0", - "expo-updates": "~0.18.15", + "expo-updates": "~0.18.19", "graphql": "^16.8.1", - "intl": "^1.2.5", "markdown-it-flowdock": "^0.3.8", "react": "18.2.0", "react-dom": "18.2.0", "react-hook-form": "^7.43.9", - "react-native": "0.72.5", + "react-native": "0.72.10", "react-native-animated-progress": "^1.0.2", "react-native-auto-grow-textinput": "^1.2.1", "react-native-dotenv": "^3.3.1", @@ -69,18 +78,22 @@ "react-native-skeleton-placeholder": "^5.2.4", "react-native-svg": "13.9.0", "react-native-toast-message": "^2.1.5", - "use-debounce": "^8.0.4" + "use-debounce": "^8.0.4", + "zod": "^3.22.4" }, "devDependencies": { "@babel/core": "^7.19.3", "@babel/plugin-proposal-nullish-coalescing-operator": "^7.18.6", "@babel/plugin-proposal-optional-chaining": "^7.21.0", + "@config-plugins/detox": "^6.0.0", "@graphql-codegen/add": "^3.2.1", "@graphql-codegen/cli": "^3.3.0", "@graphql-codegen/near-operation-file-preset": "^2.4.0", "@graphql-codegen/typescript": "2.7.2", "@graphql-codegen/typescript-operations": "^3.0.3", "@graphql-codegen/typescript-react-apollo": "3.3.2", + "@graphql-tools/mock": "^9.0.0", + "@graphql-tools/schema": "^10.0.0", "@types/apollo-upload-client": "^17.0.2", "@types/jest": "^26.0.9", "@types/react": "~18.2.14", @@ -88,12 +101,17 @@ "@types/react-native-animated-progress": "^1.0.2", "@types/react-native-dotenv": "^0.2.0", "@types/react-test-renderer": "^18.0.0", + "apollo-server-core": "^3.13.0", + "apollo-server-express": "^3.13.0", "cross-env": "^7.0.3", + "detox": "^20.11.0", "eslint": "^7.6.0", "eslint-config-kodefox": "^1.2.0", "eslint-plugin-styles": "^0.1.0", "eslint-plugin-t": "^1.2.1", + "express": "^4.19.2", "jest": "^29.2.1", + "jest-circus": "^27.4.6", "jest-expo": "^49.0.0", "patch-package": "^6.4.7", "postinstall-postinstall": "^2.1.0", @@ -104,7 +122,8 @@ "react-test-renderer": "18.0.0", "runtypes": "^5.0.1", "ts-jest": "^26.1.4", - "typescript": "^5.1.3" + "typescript": "^5.1.3", + "zod": "^3.22.4" }, "jest": { "preset": "jest-expo", @@ -136,5 +155,7 @@ "trailingComma": "all", "arrowParens": "always" }, - "private": true + "private": true, + "name": "frontend", + "version": "1.0.0" } diff --git a/frontend/patches/react-native-reanimated+3.3.0.patch b/frontend/patches/react-native-reanimated+3.3.0.patch new file mode 100644 index 00000000..e188a43c --- /dev/null +++ b/frontend/patches/react-native-reanimated+3.3.0.patch @@ -0,0 +1,13 @@ +diff --git a/node_modules/react-native-reanimated/react-native-reanimated.d.ts b/node_modules/react-native-reanimated/react-native-reanimated.d.ts +index a153973..45bca3c 100644 +--- a/node_modules/react-native-reanimated/react-native-reanimated.d.ts ++++ b/node_modules/react-native-reanimated/react-native-reanimated.d.ts +@@ -154,7 +154,7 @@ declare module 'react-native-reanimated' { + getNode(): ReactNativeFlatList; + } + // eslint-disable-next-line @typescript-eslint/no-empty-interface +- export interface FlatList extends ReactNativeView {} ++ export interface FlatList extends ReactNativeFlatList {} + + type Options

= { + setNativeProps: (ref: any, props: P) => void; diff --git a/frontend/scripts/android-E2E.sh b/frontend/scripts/android-E2E.sh new file mode 100644 index 00000000..8aa7672d --- /dev/null +++ b/frontend/scripts/android-E2E.sh @@ -0,0 +1,14 @@ +#!/bin/bash + +# Run expo client +cd frontend +yarn start:android:test &>/dev/null & +EXPO_PID=$! + +# Run detox +yarn tests:android:test +DETOX_EXIT_CODE=$? + +kill $EXPO_PID + +exit $DETOX_EXIT_CODE diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 859968b5..30fcf26e 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,5 +1,4 @@ import React from 'react'; -import { Platform } from 'react-native'; import { SafeAreaProvider } from 'react-native-safe-area-context'; import { ApolloProvider } from '@apollo/client'; import { FormProvider, useForm } from 'react-hook-form'; @@ -20,20 +19,6 @@ import { NewPostForm } from './types'; import { FORM_DEFAULT_VALUES } from './constants'; import ErrorBoundary from './components/ErrorBoundary'; -if (Platform.OS === 'android') { - require('intl'); - require('intl/locale-data/jsonp/en-US'); - // required by https://github.com/andyearnshaw/Intl.js/issues/231 - - if (Platform.OS === 'android') { - // See https://github.com/expo/expo/issues/6536 for this issue. - // eslint-disable-next-line @typescript-eslint/no-explicit-any, no-underscore-dangle - if (typeof (Intl as any).__disableRegExpRestore === 'function') { - // eslint-disable-next-line no-underscore-dangle, @typescript-eslint/no-explicit-any - (Intl as any).__disableRegExpRestore(); - } - } -} if (__DEV__) { require('react-native-console-time-polyfill'); } diff --git a/frontend/src/__mocks__/mockData.ts b/frontend/src/__mocks__/mockData.ts index 4e525431..cae992b1 100644 --- a/frontend/src/__mocks__/mockData.ts +++ b/frontend/src/__mocks__/mockData.ts @@ -8,39 +8,51 @@ const MOCK_CONTENT = const MOCK_USERS = [ { id: 1, - username: 'JohnDoe', + username: 'johndoe', name: 'John Doe', avatar: MOCK_IMG, + groups: [], + trustLevel: 3, }, { id: 2, username: 'Jeanne', name: 'Jeanne', avatar: MOCK_IMG, + groups: [], + trustLevel: 3, }, { id: 3, username: 'JackandJill', name: 'Jack and Jill', avatar: MOCK_IMG, + groups: [], + trustLevel: 3, }, { id: 4, username: 'russelthewolves', name: 'Russel Mark', avatar: MOCK_IMG, + groups: [], + trustLevel: 3, }, { id: 5, username: 'rudy_1982', name: 'Rudy Martini', avatar: MOCK_IMG, + groups: [], + trustLevel: 3, }, { id: 6, username: 'rufinannexx', name: 'Rufina Anne', avatar: MOCK_IMG, + groups: [], + trustLevel: 3, }, ]; @@ -156,7 +168,6 @@ const MOCK_NOTIFICATIONS: Array = [ name: 'Natasha Williams', message: 'Hello, Internet Citizen!', createdAt: '2020-09-11T01:15:15.634Z', - hasIcon: true, seen: false, notificationType: 1, onPress: () => {}, @@ -166,7 +177,6 @@ const MOCK_NOTIFICATIONS: Array = [ name: 'Jacobs Anderson', message: 'Like your post on Best Game 2020', createdAt: '2020-09-11T01:15:15.634Z', - hasIcon: false, seen: false, notificationType: 2, onPress: () => {}, @@ -176,7 +186,6 @@ const MOCK_NOTIFICATIONS: Array = [ name: 'Jacobs Anderson', message: 'Reply to your post on Best Game 2020', createdAt: '2020-09-11T01:15:15.634Z', - hasIcon: false, seen: true, notificationType: 3, onPress: () => {}, @@ -186,7 +195,6 @@ const MOCK_NOTIFICATIONS: Array = [ name: 'Michael Andrews', message: 'Best Game 2020', createdAt: '2020-09-11T01:15:15.634Z', - hasIcon: true, seen: true, notificationType: 1, onPress: () => {}, diff --git a/frontend/src/components/Author.tsx b/frontend/src/components/Author.tsx index 8a1e9667..8c703985 100644 --- a/frontend/src/components/Author.tsx +++ b/frontend/src/components/Author.tsx @@ -26,6 +26,7 @@ type Props = TouchableOpacityProps & onPressEmptySpaceInPost?: () => void; showStatus?: boolean; emojiCode?: string; + testIDStatus?: string; }; export function Author(props: Props) { @@ -47,6 +48,7 @@ export function Author(props: Props) { onPressEmptySpaceInPost, showStatus, emojiCode, + testIDStatus, ...otherProps } = props; @@ -59,6 +61,7 @@ export function Author(props: Props) { label={avatarLabel} style={[styles.image, imageStyle]} onPress={() => onPressAuthor && onPressAuthor(title)} + testID="Author:Avatar" /> @@ -66,7 +69,11 @@ export function Author(props: Props) { {title} {showStatus && emojiCode && ( - + )} {subtitle && ( diff --git a/frontend/src/components/BottomMenu.tsx b/frontend/src/components/BottomMenu.tsx index 936b1a31..0619ef8e 100644 --- a/frontend/src/components/BottomMenu.tsx +++ b/frontend/src/components/BottomMenu.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { Keyboard, Platform, View } from 'react-native'; +import { Keyboard, Platform, ScrollView, View } from 'react-native'; import { Divider, Icon } from '../core-ui'; import { makeStyles, useTheme } from '../theme'; @@ -9,6 +9,11 @@ type Props = { onInsertImage: () => void; onInsertLink: () => void; onInsertPoll?: () => void; + onBold: () => void; + onItalic: () => void; + onQuote?: () => void; + onBulletedList: () => void; + onNumberedList: () => void; showLeftMenu?: boolean; }; @@ -24,6 +29,11 @@ export function BottomMenu(props: Props) { onInsertImage, onInsertLink, onInsertPoll, + onBold, + onItalic, + onQuote, + onBulletedList, + onNumberedList, showLeftMenu = true, } = props; @@ -36,8 +46,60 @@ export function BottomMenu(props: Props) { return ( {showLeftMenu && ( - + + + + + + + + + + + {onInsertPoll && @@ -64,11 +127,12 @@ export function BottomMenu(props: Props) { color={colors.textLighter} onPress={onInsertPoll} style={styles.iconButton} + testID="BottomMenu:IconPoll" /> )} - + )} @@ -97,15 +161,14 @@ const useStyles = makeStyles(({ colors, shadow, spacing }) => ({ alignItems: 'stretch', ...shadow, }, + marginBottom: { marginBottom: spacing.s }, row: { flexDirection: 'row', }, leftMenu: { flex: 1, - alignItems: 'flex-start', }, rightMenu: { - flex: 1, alignItems: 'flex-end', }, iconButton: { diff --git a/frontend/src/components/CustomFlatList/CustomFlatList.tsx b/frontend/src/components/CustomFlatList/CustomFlatList.tsx index 4e8820fe..3bdd677a 100644 --- a/frontend/src/components/CustomFlatList/CustomFlatList.tsx +++ b/frontend/src/components/CustomFlatList/CustomFlatList.tsx @@ -12,7 +12,6 @@ import { FlatList, FlatListProps, ListRenderItemInfo, - LayoutChangeEvent, } from 'react-native'; /** @@ -26,7 +25,7 @@ declare module 'react' { export type RenderItemCustomOption = { isItemLoading: boolean; - onLayout: (params: { event: LayoutChangeEvent }) => void; + onLayout: () => void; }; type Props = Omit< diff --git a/frontend/src/components/Header/CustomHeader.tsx b/frontend/src/components/Header/CustomHeader.tsx index e7d3be8e..17ab20be 100644 --- a/frontend/src/components/Header/CustomHeader.tsx +++ b/frontend/src/components/Header/CustomHeader.tsx @@ -92,6 +92,7 @@ export function CustomHeader(props: Props) { navigation.navigate(prevScreen, { backToTop: false }) : navigation.goBack(); }} + testID="HeaderBackButton" /> ); }, [routesLength, isLoading, navigation, prevScreen, colors, fontSizes]); diff --git a/frontend/src/components/Header/HeaderItem.tsx b/frontend/src/components/Header/HeaderItem.tsx index 9f6f4d3a..9423c79c 100644 --- a/frontend/src/components/Header/HeaderItem.tsx +++ b/frontend/src/components/Header/HeaderItem.tsx @@ -60,6 +60,7 @@ function LabelWithIcon(props: LabelWithIcon) { onPress={onPressItem} {...otherProps} style={[!left && styles.right, style]} + testID="HeaderItem:IconWithLabel" /> ); } @@ -77,6 +78,7 @@ function IconOnly(props: IconOnlyProps) { onPress={onPressItem} {...otherProps} style={[!left && styles.right, style]} + testID="HeaderItem:IconOnly" /> ); } @@ -91,6 +93,7 @@ function LabelOnly(props: LabelOnlyProps) { onPress={onPressItem} {...otherProps} style={[!left && styles.right, style]} + testID="HeaderItem:LabelOnly" > {label} diff --git a/frontend/src/components/MentionList.tsx b/frontend/src/components/MentionList.tsx index 6cd528ab..512046ba 100644 --- a/frontend/src/components/MentionList.tsx +++ b/frontend/src/components/MentionList.tsx @@ -65,6 +65,7 @@ export function MentionList(props: Props) { data={members} style={[styles.mentionFlatlist, viewStyle]} keyExtractor={(item, index) => index.toString()} + keyboardShouldPersistTaps="handled" renderItem={({ item }) => { return ( onPressReply?.({ postId, topicId })} + testID="Metrics:Replies" /> )} diff --git a/frontend/src/components/NestedComment.tsx b/frontend/src/components/NestedComment.tsx index 7bd53637..d1423702 100644 --- a/frontend/src/components/NestedComment.tsx +++ b/frontend/src/components/NestedComment.tsx @@ -1,5 +1,5 @@ -import React, { useState } from 'react'; -import { LayoutChangeEvent, View, ViewProps } from 'react-native'; +import React, { useEffect, useState } from 'react'; +import { View, ViewProps } from 'react-native'; import { ActivityIndicator, Divider, Icon } from '../core-ui'; import { @@ -44,11 +44,11 @@ type Props = Omit & showOptions: boolean; hasMetrics?: boolean; isLoading?: boolean; - onLayout?: (params: { event: LayoutChangeEvent }) => void; + onLayout?: () => void; onPressReply?: (params: PressReplyParams) => void; onPressMore?: (params: PressMoreParams) => void; onPressAuthor?: (username: string) => void; - } & Post; + } & Post & { testIDStatus?: string }; function BaseNestedComment(props: Props) { const storage = useStorage(); @@ -82,6 +82,7 @@ function BaseNestedComment(props: Props) { emojiStatus, polls, pollsVotes, + testIDStatus, ...otherProps } = props; @@ -99,6 +100,15 @@ function BaseNestedComment(props: Props) { }, }); + /** + * Move onLayout for scroll index using useEffect because onLayout inside FlatList's view sometimes does not get called. + * this onLayout use for scroll to Index inside `CustomFlatList` + */ + + useEffect(() => { + onLayout && onLayout(); + }, [id, onLayout]); + const onPressViewIgnoredContent = () => { if (content === '') { postRaw({ variables: { postId: id } }); @@ -132,11 +142,7 @@ function BaseNestedComment(props: Props) { }; return ( - onLayout?.({ event })} - {...otherProps} - > + onPressAuthor && onPressAuthor(username)} showStatus emojiCode={emojiStatus} + testIDStatus={testIDStatus} > {showOptions ? ( + @@ -35,12 +35,14 @@ export function PollChoiceCard(props: Props) { color={colors.textLighter} style={styles.ediIcon} onPress={onEdit} + testID="PollChoiceCard:Icon:Edit" /> diff --git a/frontend/src/components/Poll/PollPreview.tsx b/frontend/src/components/Poll/PollPreview.tsx index 4a1a2f14..580c840f 100644 --- a/frontend/src/components/Poll/PollPreview.tsx +++ b/frontend/src/components/Poll/PollPreview.tsx @@ -253,7 +253,7 @@ export function PollPreview(props: Props) { let loading = loadingVote || loadingToggel || loadingUndoVote; return ( - + {title && ( {title} diff --git a/frontend/src/components/PostItem/PostDetailHeaderItem.tsx b/frontend/src/components/PostItem/PostDetailHeaderItem.tsx index cadabc41..e679763a 100644 --- a/frontend/src/components/PostItem/PostDetailHeaderItem.tsx +++ b/frontend/src/components/PostItem/PostDetailHeaderItem.tsx @@ -92,6 +92,7 @@ function BasePostDetailHeaderItem(props: Props) { polls={polls} pollsVotes={pollsVotes} postId={postId} + testIDStatus="PostDetailHeaderItem:Author:EmojiStatus" footer={ ; pollsVotes?: Array; postId?: number; + testIDStatus?: string; }; function BasePostItem(props: Props) { @@ -77,6 +78,7 @@ function BasePostItem(props: Props) { polls, pollsVotes, postId, + testIDStatus, ...otherProps } = props; @@ -125,6 +127,7 @@ function BasePostItem(props: Props) { onPressEmptySpaceInPost={onPressPost} showStatus={showStatus} emojiCode={emojiCode} + testIDStatus={testIDStatus} /> ); diff --git a/frontend/src/components/PostItem/SearchPostItem.tsx b/frontend/src/components/PostItem/SearchPostItem.tsx index 79307f20..9ef00fbd 100644 --- a/frontend/src/components/PostItem/SearchPostItem.tsx +++ b/frontend/src/components/PostItem/SearchPostItem.tsx @@ -75,6 +75,7 @@ function BaseSearchPostItem(props: Props) { username={cacheSearchPost.username} isLiked={cachedSearchTopic.liked ?? false} numberOfLines={5} + testID={`Search:SearchPostItem:${topicId}`} /> ); } diff --git a/frontend/src/components/PostItem/UserInformationPostItem.tsx b/frontend/src/components/PostItem/UserInformationPostItem.tsx index c12a2909..200d4c65 100644 --- a/frontend/src/components/PostItem/UserInformationPostItem.tsx +++ b/frontend/src/components/PostItem/UserInformationPostItem.tsx @@ -45,12 +45,13 @@ function BaseUserInformationPostItem(props: Props) { let { title, - excerpt: content, + excerpt, avatarTemplate, categoryId, hidden, createdAt, username, + markdownContent: content, } = cacheUserAction; const channels = storage.getItem('channels'); @@ -67,7 +68,7 @@ function BaseUserInformationPostItem(props: Props) { topicId={topicId} title={title} currentUser={currentUser} - content={content} + content={content || excerpt} avatar={avatar} channel={channel} hidden={!!hidden} diff --git a/frontend/src/components/RepliedPost.tsx b/frontend/src/components/RepliedPost.tsx index 67bc257a..a37642ec 100644 --- a/frontend/src/components/RepliedPost.tsx +++ b/frontend/src/components/RepliedPost.tsx @@ -8,7 +8,11 @@ import { useRepliedPostQuery, } from '../generated/server'; import { client } from '../graphql/client'; -import { handleUnsupportedMarkdown, RepliedPostLoadFail } from '../helpers'; +import { + getImage, + handleUnsupportedMarkdown, + RepliedPostLoadFail, +} from '../helpers'; import { makeStyles } from '../theme'; import { Author } from './Author'; @@ -31,7 +35,7 @@ function BaseRepliedPost(props: BaseRepliedPostProps) { - {!hideAuthor && } + {!hideAuthor && } , ToastShowParams> = { visibilityTime: 5000, }, }; -let networkCheckIntervalId: number | undefined; +let networkCheckIntervalId: NodeJS.Timer | undefined; let shouldShowNetworkOnline = false; export function RequestError(props: Props) { @@ -59,7 +59,7 @@ export function RequestError(props: Props) { networkCheckIntervalId = undefined; networkStatusVar('Online'); } - }, 5000) as unknown as number; // needed as RN and node timer global conflicting https://github.com/microsoft/TypeScript/issues/37053 + }, 5000); // needed as RN and node timer global conflicting https://github.com/microsoft/TypeScript/issues/37053 } return () => { if (networkCheckIntervalId) { diff --git a/frontend/src/components/StackedAvatars.tsx b/frontend/src/components/StackedAvatars.tsx index e1e58a3b..8cc10dc3 100644 --- a/frontend/src/components/StackedAvatars.tsx +++ b/frontend/src/components/StackedAvatars.tsx @@ -32,7 +32,11 @@ export function StackedAvatars(props: Props) { const shownAvatars = avatars.slice(0, max); return ( - + {shownAvatars.map((avatar, index) => { return ( void; + onKeyPress?: (e: NativeSyntheticEvent) => void; onFocus?: (e: NativeSyntheticEvent) => void; onBlur?: (e: NativeSyntheticEvent) => void; onSelectedChange: (cursor: CursorPosition) => void; inputRef?: RefObject; mentionToggled?: boolean; isKeyboardShow: boolean; + selectionCursor?: { start: number; end: number }; }; export function TextArea(props: Props) { @@ -43,6 +46,9 @@ export function TextArea(props: Props) { mentionToggled, isKeyboardShow, onBlur, + selectionCursor, + testID, + onKeyPress, ...otherProps } = props; @@ -51,7 +57,7 @@ export function TextArea(props: Props) { // Normal variant used in NewPost and NewMessaage // Large variant used in PostReply const NORMAL_IOS_VIEW_SIZE = screen.height * 0.24; - const LARGE_IOS_VIEW_SIZE = screen.height * 0.4; + const LARGE_IOS_VIEW_SIZE = screen.height * 0.28; const IOS_VIEW_SIZE = large ? LARGE_IOS_VIEW_SIZE : NORMAL_IOS_VIEW_SIZE; const MENTION_TEXT_AREA_VIEW_SIZE = 200; @@ -76,7 +82,8 @@ export function TextArea(props: Props) { onSelectionChange={(cursor) => { onSelectedChange(cursor.nativeEvent.selection); }} - onChangeText={(value) => onChangeValue(value)} + onKeyPress={onKeyPress} + onChangeText={onChangeValue} style={ ios ? [ @@ -97,12 +104,18 @@ export function TextArea(props: Props) { ] } multiline - autoCorrect={false} + autoCorrect autoCapitalize="sentences" placeholder={placeholder} placeholderTextColor={colors.darkTextLighter} onFocus={onFocus} onBlur={onBlur} + /** + * Change selection in android is reproduce bug where cursor will always show at the end paragraph or delete all value in text input + */ + + selection={Platform.OS === 'android' ? undefined : selectionCursor} + testID={testID} > diff --git a/frontend/src/components/UserStatus.tsx b/frontend/src/components/UserStatus.tsx index a0f4e4ce..66f2e2fe 100644 --- a/frontend/src/components/UserStatus.tsx +++ b/frontend/src/components/UserStatus.tsx @@ -10,21 +10,29 @@ type Props = { showEditIcon?: boolean; onPress?: () => void; styleContainer?: StyleProp; + testID?: string; }; export function UserStatus(props: Props) { const styles = useStyles(); const { colors } = useTheme(); - const { emojiCode, status, showEditIcon, onPress, styleContainer } = props; + const { emojiCode, status, showEditIcon, onPress, styleContainer, testID } = + props; return ( - + {status.length > 15 ? `${status.slice(0, 15)}...` : status} diff --git a/frontend/src/constants/alert.ts b/frontend/src/constants/alert.ts new file mode 100644 index 00000000..da7f41e5 --- /dev/null +++ b/frontend/src/constants/alert.ts @@ -0,0 +1,5 @@ +// NOTE: In the future we would want to move other alert messages here + +export const LOGIN_LINK_SUCCESS_ALERT = t( + 'We found an account that matches, you should receive an email with a login link shortly', +); diff --git a/frontend/src/constants/defaultValues.ts b/frontend/src/constants/defaultValues.ts index 9ce0bf2e..4e24cb2d 100644 --- a/frontend/src/constants/defaultValues.ts +++ b/frontend/src/constants/defaultValues.ts @@ -52,7 +52,7 @@ export const DEFAULT_NOTIFICATION_CHANNEL_INPUT = { lightColor: '#FF231F7C', }; -export const CUSTOM_HEADER_NEW_TOKEN = 'X-Prose-Latest-Token'; +export const CUSTOM_HEADER_NEW_TOKEN = 'x-prose-latest-token'; export const FORM_DEFAULT_VALUES: NewPostForm = { title: '', diff --git a/frontend/src/constants/errorTypes.ts b/frontend/src/constants/errorTypes.ts index 1d384201..39ac424d 100644 --- a/frontend/src/constants/errorTypes.ts +++ b/frontend/src/constants/errorTypes.ts @@ -8,22 +8,30 @@ export const errorTypes = { /** * Below are the error messages that are displayed to the user. */ -export const ERROR_PAGINATION = - 'Something unexpected happened when loading items. If this persists, please contact support.'; +export const ERROR_PAGINATION = t( + 'Something unexpected happened when loading items. If this persists, please contact support.', +); export const ERROR_PRIVATE_POST = { - title: 'Private Post', - content: `We're sorry, but you don't have permission to access this private post.`, + title: t('Private Post'), + content: t( + "We're sorry, but you don't have permission to access this private post.", + ), }; export const ERROR_SETUP_PUSH_NOTIFICATIONS = { - title: 'Something went wrong', - content: 'Failed to setup push notifications', + title: t('Something went wrong'), + content: t('Failed to setup push notifications'), }; -export const ERROR_REFETCH = - 'Something went wrong when logging out. If this persists, please contact support.'; +export const ERROR_REFETCH = t( + 'Something went wrong when logging out. If this persists, please contact support.', +); export const ERROR_MESSAGE_INVALID_ACCESS = { - title: 'Permission Denied', - content: `It looks like you don't have permission to check out this message.`, + title: t('Permission Denied'), + content: t( + "It looks like you don't have permission to check out this message.", + ), }; + +export const ERROR_UNEXPECTED = t('Something went wrong please try again.'); diff --git a/frontend/src/constants/index.ts b/frontend/src/constants/index.ts index 669e5e58..459543fc 100644 --- a/frontend/src/constants/index.ts +++ b/frontend/src/constants/index.ts @@ -6,3 +6,4 @@ export * from './theme'; export * from './wordings'; export * from './links'; export * from './route'; +export * from './alert'; diff --git a/frontend/src/constants/links.ts b/frontend/src/constants/links.ts index e4d6b309..9937bf75 100644 --- a/frontend/src/constants/links.ts +++ b/frontend/src/constants/links.ts @@ -5,12 +5,15 @@ import { ObjectValues, RootStackParamList } from '../types'; /** * A mapping of all inbound deep links supported by the Mobile App. */ -export const deepRoutes = { - 'message-detail': 'message-detail', - 'post-detail': 'post-detail', -} as const; -export type DeepLinkRoute = ObjectValues; +export enum DeepRoutes { + 'message-detail' = 'message-detail', + 'post-detail' = 'post-detail', + 'activate-account' = 'activate-account', + 'email-login' = 'email-login', +} + +export type DeepLinkRoute = ObjectValues; /** * Exclude any other known deep links @@ -19,30 +22,53 @@ export type PostOrMessageDetailRoute = Extract< DeepLinkRoute, 'message-detail' | 'post-detail' >; +export type EmailLoginOrActivateAccountRoute = Extract< + DeepLinkRoute, + 'email-login' | 'activate-account' +>; + +export function isRouteAvailable(route: string): route is DeepLinkRoute { + return isPostOrMessageDetail(route) || isEmailLoginOrActivateAccount(route); +} export function isPostOrMessageDetail( route: string, ): route is PostOrMessageDetailRoute { return ( - route === deepRoutes['message-detail'] || - route === deepRoutes['post-detail'] + route === DeepRoutes['message-detail'] || + route === DeepRoutes['post-detail'] + ); +} +export function isEmailLoginOrActivateAccount( + route: string, +): route is EmailLoginOrActivateAccountRoute { + return ( + route === DeepRoutes['activate-account'] || + route === DeepRoutes['email-login'] ); } export const DEEP_LINK_SCREEN_CONFIG: PathConfigMap = { // Route `message-detail` and `post-detail` to their respective // closest screens as a fallback. - Messages: deepRoutes['message-detail'], + Messages: DeepRoutes['message-detail'], TabNav: { - path: deepRoutes['post-detail'], + path: DeepRoutes['post-detail'], }, // Route to the specific detail scenes when the path is provided // with the needed parameters. MessageDetail: { - path: `${deepRoutes['message-detail']}/t/:slug/:id/:postNumber`, + path: `${DeepRoutes['message-detail']}/t/:slug/:id/:postNumber`, }, PostDetail: { - path: `${deepRoutes['post-detail']}/t/:slug/:topicId/:postNumber?`, + path: `${DeepRoutes['post-detail']}/t/:slug/:topicId/:postNumber?`, + }, + // Two paths will redirect to the Login screen, `activate-account` and `email-login`, + // but we only listed the `email-login` because each screen has only one path. + // So for `activate-account, we handle the redirect manually through `getStateFromPath`, + // according to this comment https://github.com/react-navigation/react-navigation/issues/9328#issuecomment-1158694831 + Login: { + path: `${DeepRoutes['email-login']}/:emailToken`, }, }; diff --git a/frontend/src/constants/route.ts b/frontend/src/constants/route.ts index 40d98840..93ee88f7 100644 --- a/frontend/src/constants/route.ts +++ b/frontend/src/constants/route.ts @@ -1,60 +1,33 @@ import * as Notifications from 'expo-notifications'; import * as Linking from 'expo-linking'; -import { NotificationType } from '../types'; +import { DiscourseNotificationData, NotificationType } from '../types'; // Only for developing in Expo export const EXPO_PREFIX = Linking.createURL('/'); -type DiscourseNotificationData = { - type: string; // This is a numeric string representing `NotificationType` - discourse_url: string; - is_pm: boolean; -}; - -// TODO: #1198: replace this and its related type with proper `zod` types and `safeParse` - -function isNotificationFromDiscourse( - data: unknown, -): data is DiscourseNotificationData { - if (typeof data !== 'object' || data == null) { - return false; - } - - if (!('is_pm' in data) || typeof data.is_pm !== 'boolean') { - return false; - } - - if (!('discourse_url' in data) || typeof data.discourse_url !== 'string') { - return false; - } - if (!('type' in data) || typeof data.type !== 'string') { - return false; - } - - return true; -} - export const handleUrl = (response: Notifications.NotificationResponse) => { const { data }: { data: unknown } = response.notification.request.content; - if (!isNotificationFromDiscourse(data)) { - return EXPO_PREFIX; - } + const notificationResult = DiscourseNotificationData.safeParse(data); - const { type, discourse_url: discourseUrl, is_pm: isPm } = data; - const notificationType = Number.parseInt(type, 10); - if (Number.isNaN(notificationType)) { + if (!notificationResult.success) { return EXPO_PREFIX; } + const { + type, + discourse_url: discourseUrl, + is_pm: isPm, + } = notificationResult.data; + const sceneUrl = isPm ? `message-detail${discourseUrl}` : `post-detail${discourseUrl}`; let url = ''; - switch (notificationType) { + switch (type) { case NotificationType.Mention: case NotificationType.ReplyPost: case NotificationType.LikePost: diff --git a/frontend/src/constants/theme/fonts.ts b/frontend/src/constants/theme/fonts.ts index 72cd18fe..3d19544f 100644 --- a/frontend/src/constants/theme/fonts.ts +++ b/frontend/src/constants/theme/fonts.ts @@ -1,9 +1,11 @@ import { TextStyle } from 'react-native'; -export const FONT_VARIANTS = { - normal: { fontWeight: '400' } as TextStyle, - semiBold: { fontWeight: '600' } as TextStyle, - bold: { fontWeight: '700' } as TextStyle, +type FontVariants = Record; + +export const FONT_VARIANTS: FontVariants = { + normal: { fontWeight: '400' }, + semiBold: { fontWeight: '600' }, + bold: { fontWeight: '700' }, }; export const FONT_SIZES = { diff --git a/frontend/src/core-ui/AppleSignInButton.tsx b/frontend/src/core-ui/AppleSignInButton.tsx new file mode 100644 index 00000000..be90d275 --- /dev/null +++ b/frontend/src/core-ui/AppleSignInButton.tsx @@ -0,0 +1,35 @@ +import React from 'react'; +import * as AppleAuthentication from 'expo-apple-authentication'; + +import { makeStyles } from '../theme'; + +import { ActivityIndicator } from './ActivityIndicator'; + +type Props = { + loading?: boolean; + disabled?: boolean; + onPress: () => void; +}; + +export function AppleSignInButton(props: Props) { + const { loading, onPress, disabled } = props; + const styles = useStyles(); + + return loading ? ( + + ) : ( + {}} + /> + ); +} + +const useStyles = makeStyles(() => ({ + appleLoginButton: { width: '100%', height: 44 }, +})); diff --git a/frontend/src/core-ui/Emoji.tsx b/frontend/src/core-ui/Emoji.tsx index f2dd87c7..4d27fa21 100644 --- a/frontend/src/core-ui/Emoji.tsx +++ b/frontend/src/core-ui/Emoji.tsx @@ -19,6 +19,7 @@ type Props = Omit & { defaultImage?: boolean; onPress?: () => void; disableOnPress?: boolean; + testIDButton?: string; }; export function Emoji(props: Props) { @@ -30,6 +31,7 @@ export function Emoji(props: Props) { style, onPress, disableOnPress, + testIDButton, ...otherProps } = props; @@ -45,7 +47,11 @@ export function Emoji(props: Props) { const imgSource = { uri: emojiUrl }; return ( - + ); } diff --git a/frontend/src/core-ui/Icon.tsx b/frontend/src/core-ui/Icon.tsx index d4770e97..d0a47ebd 100644 --- a/frontend/src/core-ui/Icon.tsx +++ b/frontend/src/core-ui/Icon.tsx @@ -3,6 +3,7 @@ import { Insets, StyleProp, TouchableOpacity, + TouchableOpacityProps, View, ViewStyle, } from 'react-native'; @@ -18,7 +19,7 @@ type Props = { disabled?: boolean; style?: StyleProp; hitSlop?: Insets | null | number; -}; +} & Pick; export { Props as IconProps }; @@ -33,6 +34,7 @@ export function Icon(props: Props) { disabled = false, style, hitSlop, + testID, } = props; const Icon = Icons[name]; @@ -47,6 +49,7 @@ export function Icon(props: Props) { onPress={onPress} disabled={disabled} hitSlop={hitSlop} + testID={testID} > {icon} diff --git a/frontend/src/core-ui/index.ts b/frontend/src/core-ui/index.ts index 7b6bbd41..e4af5c88 100644 --- a/frontend/src/core-ui/index.ts +++ b/frontend/src/core-ui/index.ts @@ -17,3 +17,4 @@ export * from './RadioButton'; export * from './Text'; export * from './TextInput'; export * from './Emoji'; +export * from './AppleSignInButton'; diff --git a/frontend/src/graphql/client.ts b/frontend/src/graphql/client.ts index 2126785f..98dcee16 100644 --- a/frontend/src/graphql/client.ts +++ b/frontend/src/graphql/client.ts @@ -1,5 +1,10 @@ import { createUploadLink } from 'apollo-upload-client'; -import { ApolloClient, ApolloLink, InMemoryCache } from '@apollo/client'; +import { + ApolloClient, + ApolloLink, + InMemoryCache, + Reference, +} from '@apollo/client'; import { setContext } from '@apollo/client/link/context'; import ExpoConstants from 'expo-constants'; import { onError } from '@apollo/client/link/error'; @@ -84,7 +89,7 @@ const cache = new InMemoryCache({ if (!existing || !incoming) { return incoming || existing || undefined; } - let mergedTopics = []; + let mergedTopics: Readonly> = []; if (variables.page > 0) { mergedTopics = handleDuplicates({ newArray: incoming.topics, diff --git a/frontend/src/graphql/server/auth.ts b/frontend/src/graphql/server/auth.ts index 5fb9306d..71590310 100644 --- a/frontend/src/graphql/server/auth.ts +++ b/frontend/src/graphql/server/auth.ts @@ -1,6 +1,25 @@ import { gql } from '@apollo/client'; +export const LOGIN_OUTPUT_FRAGMENT = gql` + fragment LoginOutputFragment on LoginOutput { + token + enableLexiconPushNotifications + user { + id + name + username + avatar: avatarTemplate + trustLevel + groups { + id + name + } + } + } +`; + export const LOGIN = gql` + ${LOGIN_OUTPUT_FRAGMENT} mutation Login( $email: String! $password: String! @@ -12,18 +31,7 @@ export const LOGIN = gql` secondFactorToken: $secondFactorToken ) { ... on LoginOutput { - token - user { - id - name - username - avatar: avatarTemplate - trustLevel - groups { - id - name - } - } + ...LoginOutputFragment } ... on SecondFactorRequired { secondFactorRequired @@ -51,3 +59,36 @@ export const LOGOUT = gql` logout(username: $username, pushNotificationsToken: $pushNotificationsToken) } `; + +export const LOGIN_WITH_APPLE = gql` + ${LOGIN_OUTPUT_FRAGMENT} + mutation LoginWithApple($identityToken: String!) { + loginWithApple(identityToken: $identityToken) { + ...LoginOutputFragment + } + } +`; + +export const ACTIVATE_ACCOUNT = gql` + ${LOGIN_OUTPUT_FRAGMENT} + mutation ActivateAccount($token: String!) { + activateAccount(token: $token) { + ...LoginOutputFragment + } + } +`; + +export const REQUEST_LOGIN_LINK = gql` + mutation RequestLoginLink($login: String!) { + requestLoginLink(login: $login) + } +`; + +export const AUTHENTICATE_LOGIN_LINK = gql` + ${LOGIN_OUTPUT_FRAGMENT} + mutation AuthenticateLoginLink($token: String!) { + authenticateLoginLink(token: $token) { + ...LoginOutputFragment + } + } +`; diff --git a/frontend/src/graphql/server/getTopicDetail.ts b/frontend/src/graphql/server/getTopicDetail.ts index 4dffe533..5bcd956a 100644 --- a/frontend/src/graphql/server/getTopicDetail.ts +++ b/frontend/src/graphql/server/getTopicDetail.ts @@ -28,8 +28,8 @@ export const TOPIC_FRAGMENT = gql` views likeCount categoryId - posters { - ... on TopicPoster { + posters: postersUnion { + ... on TopicPosterNewUnion { userId description user { diff --git a/frontend/src/graphql/server/profile.ts b/frontend/src/graphql/server/profile.ts index d71b07c2..fbe8b355 100644 --- a/frontend/src/graphql/server/profile.ts +++ b/frontend/src/graphql/server/profile.ts @@ -1,10 +1,10 @@ import { gql } from '@apollo/client'; -import { USER_ACTIONS_FRAGMENT } from './userActivity'; +import { USER_STATUS_FRAGMENT } from './userStatus'; export const PROFILE = gql` + ${USER_STATUS_FRAGMENT} query Profile($username: String!) { - ${USER_ACTIONS_FRAGMENT} userProfile(username: $username) { unreadNotification user { diff --git a/frontend/src/graphql/server/site.ts b/frontend/src/graphql/server/site.ts index f3c9c916..8a241b36 100644 --- a/frontend/src/graphql/server/site.ts +++ b/frontend/src/graphql/server/site.ts @@ -48,6 +48,16 @@ export const SITE = gql` discourseBaseUrl allowPoll pollCreateMinimumTrustLevel + enableLexiconPushNotifications + } + } +`; + +export const PLUGIN_STATUS = gql` + query PluginStatus { + pluginStatus { + appleLoginEnabled + loginLinkEnabled } } `; diff --git a/frontend/src/graphql/server/topics.ts b/frontend/src/graphql/server/topics.ts index 391ccddc..14281088 100644 --- a/frontend/src/graphql/server/topics.ts +++ b/frontend/src/graphql/server/topics.ts @@ -43,8 +43,8 @@ export const TOPICS = gql` views likeCount categoryId - posters { - ... on TopicPoster { + posters: postersUnion { + ... on TopicPosterNewUnion { userId description user { diff --git a/frontend/src/graphql/server/userActivity.ts b/frontend/src/graphql/server/userActivity.ts index f56a15c4..e2767b94 100644 --- a/frontend/src/graphql/server/userActivity.ts +++ b/frontend/src/graphql/server/userActivity.ts @@ -13,6 +13,7 @@ export const USER_ACTIONS_FRAGMENT = gql` topicId username hidden + markdownContent } `; diff --git a/frontend/src/helpers/__tests__/experienceId.test.ts b/frontend/src/helpers/__tests__/experienceId.test.ts index c94618f6..c3eae0d7 100644 --- a/frontend/src/helpers/__tests__/experienceId.test.ts +++ b/frontend/src/helpers/__tests__/experienceId.test.ts @@ -1,7 +1,7 @@ import { isValidExperienceId } from '../experienceId'; it('should valid experience id', () => { - expect(isValidExperienceId('@kfox/')).toBeTruthy(); + expect(isValidExperienceId('@/')).toBeTruthy(); expect(isValidExperienceId('@username/lexicon-project')).toBeTruthy(); expect(isValidExperienceId('@anonymous/')).toBeTruthy(); }); diff --git a/frontend/src/helpers/__tests__/fontFormatting.test.ts b/frontend/src/helpers/__tests__/fontFormatting.test.ts new file mode 100644 index 00000000..4ec5972f --- /dev/null +++ b/frontend/src/helpers/__tests__/fontFormatting.test.ts @@ -0,0 +1,138 @@ +import { + formatWithNewLine, + getTextBasedCursorPosition, + separateTextByNewLine, + isLastLineBulletOrNumbering, + isEmptyBulletAndNumbering, +} from '../fontFormatting'; +describe('formatWithNewLine', () => { + it('should generate new line from previous text', () => { + const inputPreviousText = 'This is test'; + const inputPreviousText2 = 'hello\n'; + + expect(formatWithNewLine(inputPreviousText, 'Before')).toEqual( + 'This is test\n\n', + ); + expect(formatWithNewLine(inputPreviousText2, 'Before')).toEqual( + 'hello\n\n', + ); + }); + + it('should generate new line from after text', () => { + const inputAfterText = 'hello'; + const inputAfterText2 = '\njust test'; + + expect(formatWithNewLine(inputAfterText, 'After')).toEqual('\n\nhello'); + expect(formatWithNewLine(inputAfterText2, 'After')).toEqual( + '\n\njust test', + ); + }); + + it('should not generate new line', () => { + const inputText = '\n\n'; + expect(expect(formatWithNewLine(inputText, 'Before')).toEqual('\n\n')); + expect(expect(formatWithNewLine(inputText, 'After')).toEqual('\n\n')); + expect(expect(formatWithNewLine('', 'Before')).toEqual('')); + expect(expect(formatWithNewLine('', 'After')).toEqual('')); + }); +}); + +describe('separateTextByNewLine', () => { + it('should separate text by new lines and filter out empty lines', () => { + const inputText = 'This\n\nJust Test\n\n'; + const result = separateTextByNewLine(inputText); + expect(result).toEqual(['This', 'Just Test']); + }); + + it('should handle empty input text', () => { + const inputText = ''; + const result = separateTextByNewLine(inputText); + expect(result).toEqual([]); + }); + + it('should handle input with only whitespace', () => { + const inputText = ' \n \n \n'; + const result = separateTextByNewLine(inputText); + expect(result).toEqual([]); + }); +}); + +describe('getTextBasedCursorPosition', () => { + test('should correctly extract text before and after the cursor', () => { + const text = 'This is a sample text.'; + const cursorPosition = { start: 5, end: 8 }; + + const result = getTextBasedCursorPosition({ text, cursorPosition }); + + expect(result.textBeforeCursorPosition).toBe('This '); + expect(result.textAfterCursorPosition).toBe('a sample text.'); + expect(result.selectedText).toBe('is '); + }); + + test('should handle the case when no text is selected', () => { + const text = 'No text selected.'; + const cursorPosition = { start: 8, end: 8 }; + + const result = getTextBasedCursorPosition({ text, cursorPosition }); + + expect(result.textBeforeCursorPosition).toBe('No text '); + expect(result.textAfterCursorPosition).toBe('selected.'); + expect(result.selectedText).toBe(''); + }); +}); + +describe('isLastLineBulletOrNumbering', () => { + it('should check is last line bullet or numbering', () => { + const text1 = `Here List of food:\n- Pizza\n- Macaroni`; + const text2 = 'Here List of food:\n1. Pizza\n2. Macaroni'; + + expect(isLastLineBulletOrNumbering(text1)).toEqual({ + isBulletOrNumbering: true, + typeSymbol: 'Bullet', + nextNumber: undefined, + }); + + expect(isLastLineBulletOrNumbering(text2)).toEqual({ + isBulletOrNumbering: true, + typeSymbol: 'Numbering', + nextNumber: 3, + }); + }); + + it('should return invalid bullet or numbering', () => { + const text1 = 'Hello this is just empty paragraph'; + const text2 = + 'Combination list and number\n1. This is list\n- this is bullet\nempty paragraph'; + + expect(isLastLineBulletOrNumbering(text1)).toEqual({ + isBulletOrNumbering: false, + typeSymbol: undefined, + nextNumber: undefined, + }); + + expect(isLastLineBulletOrNumbering(text2)).toEqual({ + isBulletOrNumbering: false, + typeSymbol: undefined, + nextNumber: undefined, + }); + }); + describe('isEmptyBulletAndNumbering', () => { + test('should check list or number not empty', () => { + const text = '1. hello'; + const text1 = '- hello'; + const text2 = 'hello'; + + expect(isEmptyBulletAndNumbering(text)).toBeFalsy(); + expect(isEmptyBulletAndNumbering(text1)).toBeFalsy(); + expect(isEmptyBulletAndNumbering(text2)).toBeFalsy(); + }); + + test('should check list is empty', () => { + const text = '1. '; + const text1 = '- '; + + expect(isEmptyBulletAndNumbering(text)).toBeTruthy(); + expect(isEmptyBulletAndNumbering(text1)).toBeTruthy(); + }); + }); +}); diff --git a/frontend/src/helpers/__tests__/getDistanceToNow.test.ts b/frontend/src/helpers/__tests__/getDistanceToNow.test.ts index d5258a07..5b8c5a1f 100644 --- a/frontend/src/helpers/__tests__/getDistanceToNow.test.ts +++ b/frontend/src/helpers/__tests__/getDistanceToNow.test.ts @@ -32,4 +32,9 @@ it('should calculate the difference between an upcoming date and the current dat expect(getDistanceToNow(oneHourtenMinutesLater.toISOString())).toEqual( '1h 10m', ); + + const tenSecondLater = new Date(); + tenSecondLater.setSeconds(tenSecondLater.getSeconds() + 10); + + expect(getDistanceToNow(tenSecondLater.toISOString())).toEqual('10s'); }); diff --git a/frontend/src/helpers/bottomMenu.ts b/frontend/src/helpers/bottomMenu.ts index a3e25370..803b24e8 100644 --- a/frontend/src/helpers/bottomMenu.ts +++ b/frontend/src/helpers/bottomMenu.ts @@ -1,6 +1,11 @@ -import { RootStackParamList, User } from '../types'; +import { CursorPosition, RootStackParamList, User } from '../types'; import { errorHandlerAlert } from './errorHandler'; +import { + formatWithNewLine, + getTextBasedCursorPosition, + separateTextByNewLine, +} from './fontFormatting'; import { imagePickerHandler } from './imagePickerHandler'; export type BottomMenuNavigationScreens = @@ -27,6 +32,18 @@ type BottomMenuParams = { replyToPostId?: number; }; +type SetCursorPosition = (value: React.SetStateAction) => void; + +type ButtonBarParams = { + content: string; + cursorPosition: CursorPosition; + setCursorPosition: SetCursorPosition; + setValue: (name: 'raw', value: string) => void; +}; + +type FontParams = ButtonBarParams & { type: 'Bold' | 'Italic' }; +type ListParams = ButtonBarParams & { type: 'Number' | 'Bullet' }; + export function bottomMenu(params: BottomMenuParams) { let { isKeyboardShow, @@ -60,6 +77,18 @@ export function bottomMenu(params: BottomMenuParams) { return; }; + const setTextInputCursorPosition = ({ + position, + setCursorPosition, + }: { + position: CursorPosition; + setCursorPosition: SetCursorPosition; + }) => { + setTimeout(() => { + setCursorPosition(position); + }, 50); + }; + const onInsertLink = () => { if (!isKeyboardShow) { return; @@ -79,5 +108,240 @@ export function bottomMenu(params: BottomMenuParams) { navigate('NewPoll', { prevScreen }); }; - return { onInsertImage, onInsertLink, onInsertPoll }; + /** + * Handles font formatting in a text editor. This function is designed to apply + * formatting, such as making text bold or italic, based on the specified font type. + * + * @param content The current content of the editor. + * @param cursorPosition The current cursor position in the editor. + * @param setCursorPosition A function to update the cursor position in the editor. + * @param setValue A function to set the new content value in the editor. + * @param type: The font type to be applied ('Italic' or 'Bold'). + */ + + const onFontFormatting = ({ + content, + cursorPosition, + setCursorPosition, + setValue, + type, + }: FontParams) => { + const textFormattingConfig = + type === 'Bold' + ? { format: '**', positionCursor: 2 } + : { format: '*', positionCursor: 1 }; + + const { selectedText, textAfterCursorPosition, textBeforeCursorPosition } = + getTextBasedCursorPosition({ cursorPosition, text: content }); + + let newContent = ''; + if (cursorPosition.start === cursorPosition.end) { + /** + * First condition is used if there are no text highligh so cursor start and end will be same value + */ + + newContent = `${textBeforeCursorPosition}${textFormattingConfig.format}Change Text${textFormattingConfig.format}${textAfterCursorPosition}`; + + setTextInputCursorPosition({ + setCursorPosition, + position: { + start: cursorPosition.start + textFormattingConfig.positionCursor, + end: cursorPosition.end + textFormattingConfig.positionCursor + 11, + }, + }); + } else { + /** + * This condition used for highligh text + */ + + newContent = `${textBeforeCursorPosition}${textFormattingConfig.format}${selectedText}${textFormattingConfig.format}${textAfterCursorPosition}`; + + setTextInputCursorPosition({ + setCursorPosition, + position: { + start: cursorPosition.start + textFormattingConfig.positionCursor, + end: cursorPosition.end + textFormattingConfig.positionCursor, + }, + }); + } + + setValue('raw', newContent); + }; + + /** + * Handles the "Quote" action in a button bar. + * Inserts a quoted text block based on the current cursor position and selected text. + * + * @param content The current content of the editor. + * @param cursorPosition The current cursor position in the editor. + * @param setCursorPosition A function to update the cursor position in the editor. + * @param setValue A function to set the new content value in the editor. + */ + + const onQuote = ({ + content, + cursorPosition, + setCursorPosition, + setValue, + }: ButtonBarParams) => { + let newContent = ''; + + const { selectedText, textAfterCursorPosition, textBeforeCursorPosition } = + getTextBasedCursorPosition({ cursorPosition, text: content }); + + if (cursorPosition.start === cursorPosition.end) { + newContent = `${formatWithNewLine( + textBeforeCursorPosition, + 'Before', + )}> Quote Text${formatWithNewLine(textAfterCursorPosition, 'After')}`; + + setTextInputCursorPosition({ + setCursorPosition, + position: { + start: + formatWithNewLine(textBeforeCursorPosition, 'Before').length + 2, + end: + formatWithNewLine(textBeforeCursorPosition, 'Before').length + 12, + }, + }); + } else { + const selectedTextArray = separateTextByNewLine(selectedText); + + const isEmptySelectedText = selectedTextArray.length === 0; + + /** + * This condition check is selected text empty space or newline. + * If empty space we want replace it into quote default + * + * > Quote Text + */ + + if (isEmptySelectedText) { + newContent = `${formatWithNewLine( + textBeforeCursorPosition, + 'Before', + )}> Quote Text${formatWithNewLine(textAfterCursorPosition, 'After')}`; + + setTextInputCursorPosition({ + setCursorPosition, + position: { + start: + formatWithNewLine(textBeforeCursorPosition, 'Before').length + 2, + end: + formatWithNewLine(textBeforeCursorPosition, 'Before').length + 12, + }, + }); + } else { + /** + * This condition will combine selected text with new line with > at first text + * + * example + * this is just test + * Hello World + * + * became + * > this is just test + * > Hello World + */ + + const combineSelectedText = selectedTextArray.reduce( + (result, item, index, array) => + result + `> ${item}${index === array.length - 1 ? '' : '\n'}`, + '', + ); + + newContent = `${formatWithNewLine( + textBeforeCursorPosition, + 'Before', + )}${combineSelectedText}${formatWithNewLine( + textAfterCursorPosition, + 'After', + )}`; + + setTextInputCursorPosition({ + setCursorPosition, + position: { + start: + formatWithNewLine(textBeforeCursorPosition, 'Before').length + 2, + end: + formatWithNewLine(textBeforeCursorPosition, 'Before').length + + combineSelectedText.length, + }, + }); + } + } + + setValue('raw', newContent); + }; + + const onListFormatting = ({ + content, + cursorPosition, + setCursorPosition, + setValue, + type, + }: ListParams) => { + const textFormattingConfig = + type === 'Number' + ? { format: '1. ', positionCursor: 3 } + : { format: '- ', positionCursor: 2 }; + const { selectedText, textAfterCursorPosition, textBeforeCursorPosition } = + getTextBasedCursorPosition({ cursorPosition, text: content }); + + let newContent = ''; + if (cursorPosition.start === cursorPosition.end) { + /** + * First condition is used if there are no text highligh so cursor start and end will be same value + */ + + const formattedTextBeforeCursor = formatWithNewLine( + textBeforeCursorPosition, + 'Before', + ); + newContent = + formattedTextBeforeCursor + + textFormattingConfig.format + + 'List Item' + + formatWithNewLine(textAfterCursorPosition, 'After'); + + setTextInputCursorPosition({ + setCursorPosition, + position: { + start: + formattedTextBeforeCursor.length + + textFormattingConfig.positionCursor, + end: + formattedTextBeforeCursor.length + + textFormattingConfig.positionCursor + + 9, + }, + }); + } else { + /** + * This condition used for highligh text + */ + const itemList = separateTextByNewLine(selectedText.trim()) + .map((item, index) => + type === 'Number' + ? `${index + 1}. ${item}` + : `${textFormattingConfig.format} ${item}`, + ) + .join('\n'); + newContent = `${formatWithNewLine( + textBeforeCursorPosition, + 'Before', + )}${itemList}${formatWithNewLine(textAfterCursorPosition, 'After')}`; + } + + setValue('raw', newContent); + }; + + return { + onInsertImage, + onInsertLink, + onInsertPoll, + onFontFormatting, + onQuote, + onListFormatting, + }; } diff --git a/frontend/src/helpers/createCachedStorage.mock.tsx b/frontend/src/helpers/createCachedStorage.mock.tsx new file mode 100644 index 00000000..7794a718 --- /dev/null +++ b/frontend/src/helpers/createCachedStorage.mock.tsx @@ -0,0 +1,121 @@ +import React, { + createContext, + ReactElement, + useContext, + useEffect, + useMemo, + useRef, + useState, +} from 'react'; +import AsyncStorage from '@react-native-async-storage/async-storage'; + +import mockData from '../__mocks__/mockData'; +import Config from '../../Config'; + +type JsonValue = + | null + | boolean + | number + | string + | Array + | undefined + | { [key: string]: JsonValue }; + +type Reviver = (parsed: JsonValue) => T; + +// eslint-disable-next-line @typescript-eslint/ban-types +type DataStore = { + getItem: (key: Key) => T[Key] | null; + setItem: (key: Key, value: T[Key]) => void; + removeItem: (key: Key) => void; +}; + +export function createCachedStorage< + Obj extends Record, + Schema extends { + [K in keyof Obj]: Reviver; + }, + Data extends { + [K in keyof Schema]: ReturnType; + }, +>(schema: Schema, prefix = '') { + const Context = createContext | undefined>(undefined); + + let StorageProvider = (props: { children: ReactElement }) => { + let [isLoading, setLoading] = useState(true); + let dataRef = useRef>({}); + + useEffect(() => { + let data = dataRef.current; + + let load = async () => { + /** + * The part we mock for add async storage user when open app + */ + await AsyncStorage.setItem( + prefix + String('user'), + JSON.stringify(mockData.users[0]), + ); + + await AsyncStorage.setItem( + prefix + String('userStatus'), + JSON.stringify({ + emoji_set: 'twitter', + base_path: Config.proseUrl, + }), + ); + + for (let [key, reviver] of Object.entries(schema)) { + let value = await AsyncStorage.getItem(prefix + String(key)); + if (value != null) { + try { + let keySchema: keyof Schema = key; + // This will throw if the string does not parse or if the parsed + // value cannot be revived successfully. + data[keySchema] = reviver(JSON.parse(value)); + } catch (e) {} + } + } + setLoading(false); + }; + load(); + }, []); + + let context = useMemo(() => { + let data = dataRef.current; + + return { + getItem: (key: Key) => data[key] ?? null, + setItem: ( + key: Key, + value: Data[Key] | undefined, + ) => { + data[key] = value; + // TODO: Throttle this so if we write in rapid succession (such as + // onScroll saving scroll position) we won't thrash the disk. + AsyncStorage.setItem(prefix + String(key), JSON.stringify(value)); + }, + removeItem: (key: Key) => { + data[key] = undefined; + AsyncStorage.removeItem(prefix + String(key)); + }, + }; + }, []); + + return isLoading ? null : ( + {props.children} + ); + }; + + let useStorage = () => { + const context = useContext(Context); + + if (context === undefined) { + throw new Error(); + } + + return context; + }; + + return [StorageProvider, useStorage] as const; +} diff --git a/frontend/src/helpers/createCachedStorage.tsx b/frontend/src/helpers/createCachedStorage.tsx index 547ba294..f9aba966 100644 --- a/frontend/src/helpers/createCachedStorage.tsx +++ b/frontend/src/helpers/createCachedStorage.tsx @@ -1,4 +1,3 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ import React, { createContext, ReactElement, @@ -16,6 +15,7 @@ type JsonValue = | number | string | Array + | undefined | { [key: string]: JsonValue }; type Reviver = (parsed: JsonValue) => T; @@ -28,13 +28,12 @@ type DataStore = { }; export function createCachedStorage< - // eslint-disable-next-line @typescript-eslint/ban-types - Obj extends object, + Obj extends Record, Schema extends { - [K in keyof Obj]: Obj[K] extends Reviver ? Reviver : never; + [K in keyof Obj]: Reviver; }, Data extends { - [K in keyof Schema]: Schema[K] extends Reviver ? T : never; + [K in keyof Schema]: ReturnType; }, >(schema: Schema, prefix = '') { const Context = createContext | undefined>(undefined); @@ -46,14 +45,14 @@ export function createCachedStorage< useEffect(() => { let data = dataRef.current; let load = async () => { - for (let key of Object.keys(schema) as Array) { + for (let [key, reviver] of Object.entries(schema)) { let value = await AsyncStorage.getItem(prefix + String(key)); if (value != null) { - let reviver = schema[key]; try { + let keySchema: keyof Schema = key; // This will throw if the string does not parse or if the parsed // value cannot be revived successfully. - data[key] = reviver(JSON.parse(value)) as any; + data[keySchema] = reviver(JSON.parse(value)); } catch (e) {} } } @@ -66,18 +65,21 @@ export function createCachedStorage< let data = dataRef.current; return { - getItem: (key) => data[key] ?? null, - setItem: (key, value) => { + getItem: (key: Key) => data[key] ?? null, + setItem: ( + key: Key, + value: Data[Key] | undefined, + ) => { data[key] = value; // TODO: Throttle this so if we write in rapid succession (such as // onScroll saving scroll position) we won't thrash the disk. AsyncStorage.setItem(prefix + String(key), JSON.stringify(value)); }, - removeItem: (key) => { + removeItem: (key: Key) => { data[key] = undefined; AsyncStorage.removeItem(prefix + String(key)); }, - } as DataStore; + }; }, []); return isLoading ? null : ( diff --git a/frontend/src/helpers/errorHandler.ts b/frontend/src/helpers/errorHandler.ts index 18f4c634..94ff1ce2 100644 --- a/frontend/src/helpers/errorHandler.ts +++ b/frontend/src/helpers/errorHandler.ts @@ -73,7 +73,8 @@ export function errorHandlerAlert( { text: t('Close') }, { text: t('Log In'), - onPress: () => (navigate ? navigate('Login') : undefined), + onPress: () => + navigate ? navigate('Login', { emailToken: undefined }) : undefined, }, ]); return; diff --git a/frontend/src/helpers/errorMessage.ts b/frontend/src/helpers/errorMessage.ts index 2ed3dafa..fb949e7b 100644 --- a/frontend/src/helpers/errorMessage.ts +++ b/frontend/src/helpers/errorMessage.ts @@ -8,3 +8,4 @@ export let LeaveMessageError = 'Failed to leave message'; export let PollVoteFail = 'Failed to vote'; export let PollValueRequired = 'Value is required'; export let PollValueOutOfRange = 'Value out of range'; +export let DuplicatePollOptionsError = 'Poll must have different options'; diff --git a/frontend/src/helpers/experienceId.ts b/frontend/src/helpers/experienceId.ts index f05c3130..8721a0a6 100644 --- a/frontend/src/helpers/experienceId.ts +++ b/frontend/src/helpers/experienceId.ts @@ -45,6 +45,6 @@ export function getExperienceId(): ExperienceIdResult { */ export function isValidExperienceId(experienceId: string): boolean { - const regexExperienceIdFormat = /^@[\w-.]+\/[\w-<>]+$/g; + const regexExperienceIdFormat = /^@[\w.<>-]+\/[\w<>-]+$/g; return regexExperienceIdFormat.test(experienceId); } diff --git a/frontend/src/helpers/fontFormatting.ts b/frontend/src/helpers/fontFormatting.ts new file mode 100644 index 00000000..c7866c37 --- /dev/null +++ b/frontend/src/helpers/fontFormatting.ts @@ -0,0 +1,130 @@ +/** + * Generates a new line based on the given text and type. this function is used for quote, bullet, and number list + * If type is 'Previous', it checks the last two characters of the text, + * if type is 'After', it checks the first two characters of the text. + * + * @param text The input text. + * @param type The type of line to generate, either 'Previous' or 'After'. + * @returns A string representing the generated new line(s). + */ + +import { CursorPosition } from '../types'; + +export function formatWithNewLine(text: string, type: 'Before' | 'After') { + const lastTwoChars = type === 'Before' ? text.slice(-2) : text.slice(0, 2); + const lastChar = type === 'Before' ? text.slice(-1) : text.slice(0, 1); + + if (!lastChar) { + return text; + } + + if (lastTwoChars !== '\n\n') { + const newLineSuffix = lastChar === '\n' ? '\n' : '\n\n'; + const newText = + type === 'Before' ? `${text}${newLineSuffix}` : `${newLineSuffix}${text}`; + return newText; + } + + return text; +} + +/** + * Splits a given text into an array of lines based on newline characters ('\n'). + * Filters out any empty lines from the result. + * + * @param text - The input text to be separated. + * @returns An array of non-empty lines extracted from the input text. + */ + +export function separateTextByNewLine(text: string) { + const separatedText = text.split('\n'); + return separatedText.filter((line) => line.trim() !== ''); +} + +/** + * Retrieves information about the text based on the cursor position. + * + * @param text input text content. + * @param cursorPosition position cursor in text input {start:number,end:number}. + * @returns An object with information about the text before and after the cursor, + * as well as the selected text if there is a range selected. + */ + +export function getTextBasedCursorPosition({ + text, + cursorPosition, +}: { + text: string; + cursorPosition: CursorPosition; +}) { + const textBeforeCursorPosition = text.substring(0, cursorPosition.start); + + const textAfterCursorPosition = text.substring( + cursorPosition.end, + text.length, + ); + + const selectedText = + cursorPosition.start !== cursorPosition.end + ? text.substring(cursorPosition.start, cursorPosition.end) + : ''; + + return { textBeforeCursorPosition, textAfterCursorPosition, selectedText }; +} + +/** + * Checks if the last line before the cursor in the input text is a bullet or numbering. + * @param text Input text content paragraph. + * @returns An object containing information about the last line: + * - isBulletOrNumbering: Whether the last line is a bullet or numbering. + * - typeSymbol: Type of the symbol used ('Bullet' or 'Numbering'). + * - nextNumber: The next number in the numbering sequence (if applicable). + */ + +export function isLastLineBulletOrNumbering(text: string) { + let lastLine = text.match(/(?:^|)(\d+\.|-)([^\n]+)$/); + let typeSymbol: 'Numbering' | 'Bullet' | undefined; + let nextNumber; + const isBulletOrNumbering = !!lastLine && lastLine.length > 0; + if (lastLine && lastLine.length > 0) { + const symbol = lastLine[1]; + if (symbol === '-') { + typeSymbol = 'Bullet'; + } else { + nextNumber = Number(symbol.replace('.', '')) + 1; + typeSymbol = 'Numbering'; + } + } + return { isBulletOrNumbering, typeSymbol, nextNumber }; +} + +/** + * Checks if the provided text represents an empty bullet or numbering list. + * @param text The input text to be checked. + * @returns A boolean indicating whether the text represents an empty bullet or numbering list. + * + * Note Text input in here not paragraph just one line sentence + */ + +export function isEmptyBulletAndNumbering(text: string) { + const emptyListRegex = /^(\d+\.\s*|-\s*)$/; + return emptyListRegex.test(text); +} + +/** + * Deletes the last line from the input text if it represents an empty bullet or numbering list. + * @param text The input text paragraph from which the last line is to be deleted. + * @returns An object containing the modified text with the last line removed if it represented an empty bullet or numbering list, and a boolean indicating whether the list was empty or not. + */ + +export function deleteLastLineEmptyBulletOrNumberingList(text: string) { + let lines = text.split('\n'); + let newText = text; + let isEmptyList = false; + if (isEmptyBulletAndNumbering(lines[lines.length - 1])) { + lines[lines.length - 1] = ''; + newText = lines.join('\n'); + isEmptyList = true; + } + return { text: newText, isEmptyBulletAndNumbering: isEmptyList }; +} diff --git a/frontend/src/helpers/getDistanceToNow.ts b/frontend/src/helpers/getDistanceToNow.ts index 3eddd809..ec1f0e84 100644 --- a/frontend/src/helpers/getDistanceToNow.ts +++ b/frontend/src/helpers/getDistanceToNow.ts @@ -17,7 +17,7 @@ export function getDistanceToNow(date: string) { return undefined; } - const { years, months, weeks, days, hours, minutes } = addWeeks( + const { years, months, weeks, days, hours, minutes, seconds } = addWeeks( intervalToDuration({ start: now, end: endDate, @@ -44,5 +44,9 @@ export function getDistanceToNow(date: string) { if (minutes) { result.push(`${minutes}m`); } + if (!hours && !minutes && seconds) { + result.push(`${seconds}s`); + } + return result.join(' '); } diff --git a/frontend/src/helpers/getExpoPushTokenHandler.mock.ts b/frontend/src/helpers/getExpoPushTokenHandler.mock.ts new file mode 100644 index 00000000..3669bf22 --- /dev/null +++ b/frontend/src/helpers/getExpoPushTokenHandler.mock.ts @@ -0,0 +1,19 @@ +import * as Device from 'expo-device'; + +import { mockToken } from '../../e2e/apollo-mock/data'; + +export async function getExpoPushTokenHandler() { + if (!Device.isDevice) { + return { + success: false, + message: 'PushNotificationsNotSupported: Must use physical device.', + token: null, + }; + } + + return { + success: true, + message: '', + token: mockToken, + }; +} diff --git a/frontend/src/helpers/getTextInputRules.ts b/frontend/src/helpers/getTextInputRules.ts index 933a73b7..482c8177 100644 --- a/frontend/src/helpers/getTextInputRules.ts +++ b/frontend/src/helpers/getTextInputRules.ts @@ -32,6 +32,19 @@ export function getTextInputRules({ maxLength: maxUsernameLength, }), }, + validate: { + startsWith: (value: string) => + /^[a-zA-Z0-9_]/.test(value) || + t('Username must begin with a letter, a number or an underscore'), + endsWith: (value: string) => + /[a-zA-Z0-9]$/.test(value) || + t('Username must end with a letter or a number'), + contains: (value: string) => + /^[a-zA-Z0-9_][a-zA-Z0-9-_.]*[a-zA-Z0-9]?$/.test(value) || + t( + 'Username must must only include numbers, letters, dashes, dots, and underscores', + ), + }, }; const nameInputRules = { required: fullNameRequired }; diff --git a/frontend/src/helpers/handleDuplicates.ts b/frontend/src/helpers/handleDuplicates.ts index dc5e3891..bea66f79 100644 --- a/frontend/src/helpers/handleDuplicates.ts +++ b/frontend/src/helpers/handleDuplicates.ts @@ -19,8 +19,8 @@ export function handleDuplicateRef( } export function handleDuplicates(params: { - newArray: Array | null; - oldArray: Array | null; + newArray: Readonly> | null; + oldArray: Readonly> | null; newArrayIs: 'prepended' | 'appended'; }) { const { newArray, oldArray, newArrayIs } = params; diff --git a/frontend/src/helpers/index.ts b/frontend/src/helpers/index.ts index 854573f2..ebe90dd1 100644 --- a/frontend/src/helpers/index.ts +++ b/frontend/src/helpers/index.ts @@ -27,7 +27,6 @@ export * from './handleDuplicates'; export * from './imagePickerHandler'; export * from './imageUploadHandler'; export * from './insertHyperlink'; -export * from './isFlatList'; export * from './localStorage'; export * from './mentionHelper'; export * from './messageDetailHandler'; @@ -54,3 +53,5 @@ export * from './listNumberStep'; export * from './unescapeHTML'; export * from './pollUtility'; export * from './stripHTML'; +export * from './fontFormatting'; +export * from './textArea'; diff --git a/frontend/src/helpers/isFlatList.ts b/frontend/src/helpers/isFlatList.ts deleted file mode 100644 index 281bd93c..00000000 --- a/frontend/src/helpers/isFlatList.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { FlatList } from 'react-native'; - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export function isFlatList(list: any): list is FlatList { - if (!list) { - return false; - } - return typeof list?.scrollToIndex === 'function'; -} diff --git a/frontend/src/helpers/linking.ts b/frontend/src/helpers/linking.ts index 926c919b..4f696e53 100644 --- a/frontend/src/helpers/linking.ts +++ b/frontend/src/helpers/linking.ts @@ -1,6 +1,6 @@ import { + DeepRoutes, FIRST_POST_NUMBER, - deepRoutes, PostOrMessageDetailRoute, } from '../constants'; import { reset } from '../navigation/NavigationService'; @@ -93,7 +93,7 @@ export function postOrMessageDetailPathToRoutes({ }: postOrMessageDetailPathToRoutesParams): Routes { const detailParams = getValidDetailParams(pathParams); if (!detailParams) { - return route === deepRoutes['message-detail'] + return route === DeepRoutes['message-detail'] ? [ { name: 'TabNav', state: { routes: [{ name: 'Profile' }] } }, { name: 'Messages' }, @@ -101,7 +101,7 @@ export function postOrMessageDetailPathToRoutes({ : [{ name: 'TabNav', state: { routes: [{ name: 'Home' }] } }]; } const { topicId, postNumber } = detailParams; - if (route === deepRoutes['message-detail']) { + if (route === DeepRoutes['message-detail']) { const messageParams: MessageDetailParams = { id: topicId, postNumber, @@ -154,5 +154,5 @@ export function extractPathname(url: string) { } export function isRouteBesidePost(route: string) { - return route !== deepRoutes['post-detail']; + return route !== DeepRoutes['post-detail']; } diff --git a/frontend/src/helpers/notificationHandler.ts b/frontend/src/helpers/notificationHandler.ts index b2011551..06b91ecb 100644 --- a/frontend/src/helpers/notificationHandler.ts +++ b/frontend/src/helpers/notificationHandler.ts @@ -44,6 +44,7 @@ export function notificationHandler( case NotificationType.GroupMention: case NotificationType.WatchingTopic: case NotificationType.TopicReminder: + case NotificationType.WatchingCategoryOrTag: case NotificationType.BookmarkReminder: { return navToPostDetail({ topicId, @@ -157,7 +158,7 @@ export function notificationHandler( break; } case NotificationType.ReplyMessage: { - message = t('Replied to your post on ') + topicTitle; + message = t('Replied to post on ') + topicTitle; break; } case NotificationType.MovePost: { @@ -186,6 +187,10 @@ export function notificationHandler( message = t('Bookmark reminder on ') + topicTitle; break; } + case NotificationType.WatchingCategoryOrTag: { + message = topicTitle; + break; + } } const name = @@ -205,7 +210,6 @@ export function notificationHandler( name, message, createdAt, - hasIcon: notificationType === 6 || notificationType === 7, seen, notificationType, onPress, @@ -223,7 +227,6 @@ export function notificationHandler( name: groupName, message, createdAt, - hasIcon: false, seen, notificationType, onPress, @@ -237,7 +240,6 @@ export function notificationHandler( name: data.badgeName, message: 'Got a new badge', createdAt, - hasIcon: false, seen, notificationType, onPress, @@ -250,7 +252,6 @@ export function notificationHandler( name: data.displayUsername, message: `${data.displayUsername} accepted your invitation.`, createdAt, - hasIcon: false, seen, notificationType, onPress, @@ -274,7 +275,6 @@ export function notificationHandler( name, message, createdAt, - hasIcon: false, seen, notificationType, onPress, diff --git a/frontend/src/helpers/paginationHandler.ts b/frontend/src/helpers/paginationHandler.ts index c9789740..caf4e9a7 100644 --- a/frontend/src/helpers/paginationHandler.ts +++ b/frontend/src/helpers/paginationHandler.ts @@ -1,10 +1,11 @@ /* eslint-disable no-underscore-dangle */ -/* eslint-disable @typescript-eslint/no-explicit-any */ import { Alert } from 'react-native'; import { FieldPolicy } from '@apollo/client'; import { Reference } from '@apollo/client/utilities'; +import { SafeReadonly } from '@apollo/client/cache/core/types/common'; import { ERROR_PAGINATION } from '../constants'; +import { MessageDetail, SearchPost, Notifications } from '../types'; import { handleDuplicateRef, handleDuplicates } from './handleDuplicates'; @@ -56,17 +57,24 @@ export function replaceDataPagination( ): FieldPolicy> { return { keyArgs, - merge: (existing: any, incoming: any) => incoming || existing || null, + merge: ( + existing: Readonly> | undefined, + incoming: Readonly>, + ) => incoming || existing || null, }; } -export function appendPagination( +export function appendPagination( keyArgs: KeyArgs = [], screen: 'HOME' | 'SEARCH' | 'MESSAGE_DETAIL' | 'NOTIFICATIONS', -): FieldPolicy> { +): FieldPolicy { return { keyArgs, - merge: (existing: any, incoming: any, { args }) => { + merge: ( + existing: SafeReadonly | undefined, + incoming: SafeReadonly, + { args }, + ) => { if (!existing || !incoming) { return existing || incoming || null; } @@ -76,37 +84,64 @@ export function appendPagination( switch (screen) { case 'SEARCH': page = args?.page || 1; - if (page > 1) { - incoming = { - ...incoming, - posts: handleDuplicateRef(existing.posts, incoming.posts), - topics: handleDuplicateRef(existing.topics, incoming.topics), - }; + + if (page > 1 && existing) { + const parsedExisting = SearchPost.safeParse(existing); + const parsedIncoming = SearchPost.safeParse(incoming); + + if (parsedExisting.success && parsedIncoming.success) { + incoming = { + ...incoming, + posts: handleDuplicateRef( + parsedExisting.data.posts, + parsedIncoming.data.posts, + ), + topics: handleDuplicateRef( + parsedExisting.data.topics, + parsedIncoming.data.topics, + ), + }; + } } break; case 'MESSAGE_DETAIL': page = args?.page || 0; - if (page >= 0) { - incoming = { - ...incoming, - users: handleDuplicateRef(incoming.users, existing.users), - topicList: { - ...incoming.topicList, - topics: handleDuplicateRef( - incoming.topicList.topics, - existing.topicList.topics, + if (page >= 0 && existing) { + const parsedExisting = MessageDetail.safeParse(existing); + const parsedIncoming = MessageDetail.safeParse(incoming); + + if (parsedExisting.success && parsedIncoming.success) { + incoming = { + ...incoming, + users: handleDuplicateRef( + parsedIncoming.data.users, + parsedExisting.data.users, ), - }, - }; + topicList: { + ...parsedIncoming.data.topicList, + topics: handleDuplicateRef( + parsedExisting.data.topicList.topics, + parsedIncoming.data.topicList.topics, + ), + }, + }; + } } break; case 'NOTIFICATIONS': - let newData = handleDuplicateRef( - existing.notifications, - incoming.notifications, - ); - incoming = { ...incoming, notifications: newData }; + if (existing) { + const parsedExisting = Notifications.safeParse(existing); + const parsedIncoming = Notifications.safeParse(incoming); + + if (parsedExisting.success && parsedIncoming.success) { + let newData = handleDuplicateRef( + parsedExisting.data.notifications, + parsedIncoming.data.notifications, + ); + incoming = { ...incoming, notifications: newData }; + } + } break; } @@ -115,12 +150,15 @@ export function appendPagination( }; } -export function prependAppendPagination( +export function prependAppendPagination( keyArgs: KeyArgs = [], ): FieldPolicy> { return { keyArgs, - merge: (existing: any, incoming: any) => { + merge: ( + existing: Readonly> | undefined, + incoming: Readonly>, + ) => { if ( !existing || !incoming || @@ -158,7 +196,7 @@ export function prependAppendPagination( * @returns It will return a number if the format is correct, and it will return undefined if the format is incorrect. */ -export function getLatestApolloId( +export function getLatestApolloId( items: Readonly>, ): number | undefined { if (!Array.isArray(items)) { @@ -188,8 +226,8 @@ export function getLatestApolloId( } type MergeReferenceDataParam = { - existing: Array; - incoming: Array; + existing: Readonly>; + incoming: Readonly>; lastExisting?: number; lastIncoming?: number; mockAlert?: (error: string) => void; @@ -201,7 +239,7 @@ export function mergeReferenceData({ lastIncoming, mockAlert, }: MergeReferenceDataParam) { - let mergedTopics = []; + let mergedTopics: Readonly> = []; /** * In this condition is check is format not PostId:number diff --git a/frontend/src/helpers/storage.mock.ts b/frontend/src/helpers/storage.mock.ts new file mode 100644 index 00000000..a510b377 --- /dev/null +++ b/frontend/src/helpers/storage.mock.ts @@ -0,0 +1,13 @@ +let token = '1234567890'; + +export let setToken = (userToken: string) => { + return (token = userToken); +}; + +export let getToken = async () => { + return token; +}; + +export let removeToken = async () => { + return (token = ''); +}; diff --git a/frontend/src/helpers/textArea.ts b/frontend/src/helpers/textArea.ts new file mode 100644 index 00000000..f2fc2228 --- /dev/null +++ b/frontend/src/helpers/textArea.ts @@ -0,0 +1,75 @@ +import { + NativeSyntheticEvent, + Platform, + TextInputKeyPressEventData, +} from 'react-native'; + +import { CursorPosition } from '../types'; + +import { + deleteLastLineEmptyBulletOrNumberingList, + getTextBasedCursorPosition, + isLastLineBulletOrNumbering, +} from './fontFormatting'; + +type OnKeyPressParams = { + event: NativeSyntheticEvent; + cursorPosition: CursorPosition; + text: string; + onChange: (...event: Array) => void; +}; + +export function onKeyPress({ + event, + cursorPosition, + text, + onChange, +}: OnKeyPressParams) { + if (event.nativeEvent.key === 'Enter') { + let newText = ''; + const { textBeforeCursorPosition, textAfterCursorPosition } = + getTextBasedCursorPosition({ + text, + cursorPosition, + }); + + /** + * This condition is added due to the difference in event handling between Android and iOS. + * In iOS, the `onKeyPress` event is triggered first, while in Android, the `onChange` event runs first. + * This leads to a disparity in the text content between iOS and Android, where in Android, the text + * at the cursor position reflects the text after pressing 'Enter', while in iOS, it represents the + * text before the last 'Enter'. + * + * Reference: https://github.com/facebook/react-native/issues/18221 + */ + + const previousTextBeforeCursorPlatform = + Platform.OS === 'android' + ? textBeforeCursorPosition.substring( + 0, + textBeforeCursorPosition.lastIndexOf('\n'), + ) + : textBeforeCursorPosition; + const checkBulletNumbering = isLastLineBulletOrNumbering( + previousTextBeforeCursorPlatform, + ); + if (checkBulletNumbering.isBulletOrNumbering) { + let checkResultDelete = deleteLastLineEmptyBulletOrNumberingList( + previousTextBeforeCursorPlatform, + ); + if (checkResultDelete.isEmptyBulletAndNumbering) { + newText = `${checkResultDelete.text}${textAfterCursorPosition}`; + } else { + if (checkBulletNumbering.typeSymbol === 'Bullet') { + newText = `${previousTextBeforeCursorPlatform}\n- ${textAfterCursorPosition}`; + } else { + newText = `${previousTextBeforeCursorPlatform}\n${checkBulletNumbering.nextNumber}. ${textAfterCursorPosition}`; + } + } + + setTimeout(() => { + onChange(newText); + }, 100); + } + } +} diff --git a/frontend/src/helpers/transformTopicToPost.ts b/frontend/src/helpers/transformTopicToPost.ts index e23790cd..83556dc7 100644 --- a/frontend/src/helpers/transformTopicToPost.ts +++ b/frontend/src/helpers/transformTopicToPost.ts @@ -33,12 +33,12 @@ let transformTopicToPost = ({ channels, imageUrl, }: Params): PostWithoutId => { - const author = posters.find((poster) => { + const author = posters?.find((poster) => { return 'userId' in poster && poster.userId === authorUserId; }); const frequentUserArray: Array = []; - posters.forEach((poster) => { + posters?.forEach((poster) => { if ('user' in poster && poster.user) { const { user } = poster; frequentUserArray.push({ @@ -56,7 +56,7 @@ let transformTopicToPost = ({ const authorUser = // eslint-disable-next-line no-underscore-dangle - author?.__typename === 'TopicPoster' ? author.user : undefined; + author?.__typename === 'TopicPosterNewUnion' ? author.user : undefined; return { topicId: id, diff --git a/frontend/src/hooks/auth/useActivateAccount.ts b/frontend/src/hooks/auth/useActivateAccount.ts new file mode 100644 index 00000000..bacebbd5 --- /dev/null +++ b/frontend/src/hooks/auth/useActivateAccount.ts @@ -0,0 +1,22 @@ +import { MutationHookOptions } from '@apollo/client'; + +import { + ActivateAccountMutation as ActivateAccountType, + ActivateAccountMutationVariables, +} from '../../generated/server'; +import { ACTIVATE_ACCOUNT } from '../../graphql/server/auth'; +import { useMutation } from '../../utils'; + +export function useActivateAccount( + options?: MutationHookOptions< + ActivateAccountType, + ActivateAccountMutationVariables + >, +) { + const [activateAccount, { loading, error }] = useMutation< + ActivateAccountType, + ActivateAccountMutationVariables + >(ACTIVATE_ACCOUNT, { ...options }); + + return { activateAccount, loading, error }; +} diff --git a/frontend/src/hooks/auth/useAuthenticateLoginLink.ts b/frontend/src/hooks/auth/useAuthenticateLoginLink.ts new file mode 100644 index 00000000..85c0d852 --- /dev/null +++ b/frontend/src/hooks/auth/useAuthenticateLoginLink.ts @@ -0,0 +1,24 @@ +import { MutationHookOptions } from '@apollo/client'; + +import { + AuthenticateLoginLinkMutation as AuthenticateLoginLinkType, + AuthenticateLoginLinkMutationVariables, +} from '../../generated/server'; +import { AUTHENTICATE_LOGIN_LINK } from '../../graphql/server/auth'; +import { useMutation } from '../../utils'; + +export function useAuthenticateLoginLink( + options?: MutationHookOptions< + AuthenticateLoginLinkType, + AuthenticateLoginLinkMutationVariables + >, +) { + const [authenticateLoginLink, { loading, error }] = useMutation< + AuthenticateLoginLinkType, + AuthenticateLoginLinkMutationVariables + >(AUTHENTICATE_LOGIN_LINK, { + ...options, + }); + + return { authenticateLoginLink, loading, error }; +} diff --git a/frontend/src/hooks/auth/useLoginWithApple.ts b/frontend/src/hooks/auth/useLoginWithApple.ts new file mode 100644 index 00000000..a613cfc9 --- /dev/null +++ b/frontend/src/hooks/auth/useLoginWithApple.ts @@ -0,0 +1,24 @@ +import { MutationHookOptions } from '@apollo/client'; + +import { + LoginWithAppleMutation as LoginWithAppleType, + LoginWithAppleMutationVariables, +} from '../../generated/server'; +import { LOGIN_WITH_APPLE } from '../../graphql/server/auth'; +import { useMutation } from '../../utils'; + +export function useLoginWithApple( + options?: MutationHookOptions< + LoginWithAppleType, + LoginWithAppleMutationVariables + >, +) { + const [loginWithApple, { loading, error }] = useMutation< + LoginWithAppleType, + LoginWithAppleMutationVariables + >(LOGIN_WITH_APPLE, { + ...options, + }); + + return { loginWithApple, loading, error }; +} diff --git a/frontend/src/hooks/auth/useLogout.ts b/frontend/src/hooks/auth/useLogout.ts index 5522f912..7e4e31df 100644 --- a/frontend/src/hooks/auth/useLogout.ts +++ b/frontend/src/hooks/auth/useLogout.ts @@ -16,12 +16,22 @@ export function useLogout( LogoutMutationVariables >(LOGOUT, { ...options }); - const logout = async ({ username }: { username: string }) => { - const { token } = await getExpoPushTokenHandler(); + const logout = async ({ + username, + enableLexiconPushNotifications, + }: { + username: string; + enableLexiconPushNotifications: boolean; + }) => { + let pushNotificationsToken = undefined; + if (enableLexiconPushNotifications) { + const { token } = await getExpoPushTokenHandler(); + pushNotificationsToken = token; + } mutate({ variables: { - pushNotificationsToken: token, + pushNotificationsToken, username, }, }); diff --git a/frontend/src/hooks/auth/useRequestLoginLink.ts b/frontend/src/hooks/auth/useRequestLoginLink.ts new file mode 100644 index 00000000..0eeb94a0 --- /dev/null +++ b/frontend/src/hooks/auth/useRequestLoginLink.ts @@ -0,0 +1,24 @@ +import { MutationHookOptions } from '@apollo/client'; + +import { + RequestLoginLinkMutation as RequestLoginLinkType, + RequestLoginLinkMutationVariables, +} from '../../generated/server'; +import { REQUEST_LOGIN_LINK } from '../../graphql/server/auth'; +import { useMutation } from '../../utils'; + +export function useRequestLoginLink( + options?: MutationHookOptions< + RequestLoginLinkType, + RequestLoginLinkMutationVariables + >, +) { + const [requestLoginLink, { loading, error }] = useMutation< + RequestLoginLinkType, + RequestLoginLinkMutationVariables + >(REQUEST_LOGIN_LINK, { + ...options, + }); + + return { requestLoginLink, loading, error }; +} diff --git a/frontend/src/hooks/index.ts b/frontend/src/hooks/index.ts index 830096e8..2341df74 100644 --- a/frontend/src/hooks/index.ts +++ b/frontend/src/hooks/index.ts @@ -3,6 +3,10 @@ export * from './auth/useLogout'; export * from './auth/usePushNotifications'; export * from './auth/useRefreshToken'; export * from './auth/useRegister'; +export * from './auth/useLoginWithApple'; +export * from './auth/useActivateAccount'; +export * from './auth/useRequestLoginLink'; +export * from './auth/useAuthenticateLoginLink'; export * from './post/useActivity'; export * from './post/useEditPost'; diff --git a/frontend/src/hooks/post/useLikeTopicOrPost.ts b/frontend/src/hooks/post/useLikeTopicOrPost.ts index fd59811c..1ba898eb 100644 --- a/frontend/src/hooks/post/useLikeTopicOrPost.ts +++ b/frontend/src/hooks/post/useLikeTopicOrPost.ts @@ -109,7 +109,10 @@ const optimisticResponse: MutationOptimisticResponse = ({ const refetchQueries: MutationRefetchQueries = ({ data }) => { const topicDetailQuery = { query: GetTopicDetailDocument, - variables: { topicId: data?.likeTopicOrPost.topicId }, + variables: { + topicId: data?.likeTopicOrPost.topicId, + includeFirstPost: true, + }, }; return [TOPICS, topicDetailQuery]; }; diff --git a/frontend/src/hooks/post/useLoadMorePost.ts b/frontend/src/hooks/post/useLoadMorePost.ts index 8d3cd87f..0ecea18a 100644 --- a/frontend/src/hooks/post/useLoadMorePost.ts +++ b/frontend/src/hooks/post/useLoadMorePost.ts @@ -14,29 +14,24 @@ type FetchVariables = | GetTopicDetailQueryVariables | GetMessageDetailQueryVariables; -export type LoadMorePostsParams< - TFetchVars extends FetchVariables, - TFetchData extends FetchData, -> = { +export type LoadMorePostsParams = { stream?: Array; loadNewerPosts: boolean; hasMorePost: boolean; lastLoadedPostIndex?: number; firstLoadedPostIndex?: number; - fetchMoreVariables?: Partial; + fetchMoreVariables?: Partial; fetchMore: ( - fetchMoreOptions: FetchMoreQueryOptions, - ) => Promise>; + fetchMoreOptions: FetchMoreQueryOptions, + ) => Promise>; }; export function useLoadMorePost(topicId: number) { - const [isLoadingOlderPost, setisLoadingOlderPost] = useState(false); - const [isLoadingNewerPost, setisLoadingNewerPost] = useState(false); + const [isLoadingOlderPost, setIsLoadingOlderPost] = useState(false); + const [isLoadingNewerPost, setIsLoadingNewerPost] = useState(false); const loadMorePosts = useCallback( - async ( - params: LoadMorePostsParams, - ) => { + async (params: LoadMorePostsParams) => { const { stream, fetchMore, @@ -57,9 +52,9 @@ export function useLoadMorePost(topicId: number) { } if (loadNewerPosts) { - setisLoadingNewerPost(true); + setIsLoadingNewerPost(true); } else { - setisLoadingOlderPost(true); + setIsLoadingOlderPost(true); } const { nextFirstLoadedPostIndex, nextLastLoadedPostIndex, postIds } = @@ -80,11 +75,11 @@ export function useLoadMorePost(topicId: number) { postIds, postNumber: undefined, ...fetchMoreVariables, - } as Partial, + }, }); - setisLoadingNewerPost(false); - setisLoadingOlderPost(false); + setIsLoadingNewerPost(false); + setIsLoadingOlderPost(false); if (error) { return; diff --git a/frontend/src/hooks/site/usePluginStatus.ts b/frontend/src/hooks/site/usePluginStatus.ts new file mode 100644 index 00000000..8a00c6f2 --- /dev/null +++ b/frontend/src/hooks/site/usePluginStatus.ts @@ -0,0 +1,31 @@ +import { QueryHookOptions } from '@apollo/client'; + +import { + PluginStatusQuery as PluginStatusType, + PluginStatusDocument, +} from '../../generated/server'; +import { ErrorAlertOptionType } from '../../types'; +import { useQuery } from '../../utils'; + +export function usePluginStatus( + options?: QueryHookOptions, + errorAlert: ErrorAlertOptionType = 'SHOW_ALERT', +) { + const { data, loading, error, refetch } = useQuery( + PluginStatusDocument, + { + ...options, + }, + errorAlert, + ); + + const { appleLoginEnabled, loginLinkEnabled } = data?.pluginStatus || {}; + + return { + appleLoginEnabled, + loginLinkEnabled, + loading, + error, + refetch, + }; +} diff --git a/frontend/src/hooks/site/useSiteSettings.ts b/frontend/src/hooks/site/useSiteSettings.ts index bb461f6c..7413a9df 100644 --- a/frontend/src/hooks/site/useSiteSettings.ts +++ b/frontend/src/hooks/site/useSiteSettings.ts @@ -36,6 +36,7 @@ export function useSiteSettings(options?: QueryHookOptions) { allowUserStatus, allowPoll, pollCreateMinimumTrustLevel, + enableLexiconPushNotifications, } = data?.site || {}; return { @@ -60,6 +61,7 @@ export function useSiteSettings(options?: QueryHookOptions) { allowUserStatus, allowPoll, pollCreateMinimumTrustLevel, + enableLexiconPushNotifications, loading, error, refetch, diff --git a/frontend/src/hooks/useKASVWorkaround.ts b/frontend/src/hooks/useKASVWorkaround.ts index 9c32e9bc..191cbba9 100644 --- a/frontend/src/hooks/useKASVWorkaround.ts +++ b/frontend/src/hooks/useKASVWorkaround.ts @@ -13,7 +13,7 @@ type ScrollToInput = KeyboardAwareScrollView['scrollToFocusedInput']; * component. */ type KASVRef = JSX.Element & { - getScrollResponder: () => { + getScrollResponder?: () => { props: { scrollToFocusedInput: ScrollToInput; }; @@ -42,13 +42,13 @@ export function useKASVWorkaround() { const scrollRef = React.useRef(); const scrollToInputRef = React.useRef(); - function innerRef(ref: JSX.Element) { + function innerRef(ref: KASVRef) { scrollRef.current = ref; if (!ref) { return; } - const refWithWorkAround = ref as unknown as KASVRef; + const refWithWorkAround = ref; const { getScrollResponder } = refWithWorkAround; if (!getScrollResponder) { diff --git a/frontend/src/hooks/useLoadFonts.ts b/frontend/src/hooks/useLoadFonts.ts index 6349326b..ddc0a58e 100644 --- a/frontend/src/hooks/useLoadFonts.ts +++ b/frontend/src/hooks/useLoadFonts.ts @@ -1,7 +1,8 @@ import React from 'react'; import * as Font from 'expo-font'; import { CourierPrime_400Regular as Courier } from '@expo-google-fonts/courier-prime'; -import { CodedError } from 'expo-modules-core'; + +import { CodedErrorExpoModuleSchema } from '../types'; const loadFonts = async () => { return Font.loadAsync({ @@ -13,15 +14,15 @@ const loadFonts = async () => { export default function useLoadFonts() { const [loading, setLoading] = React.useState(true); - const [error, setError] = React.useState(''); + const [error, setError] = React.useState(''); React.useEffect(() => { const load = async () => { try { await loadFonts(); } catch (error: unknown) { - let codedError = error as CodedError; - setError(codedError.message ?? null); + const errorResult = CodedErrorExpoModuleSchema.safeParse(error); + setError(errorResult.success ? errorResult.data.message ?? null : null); } setLoading(false); diff --git a/frontend/src/hooks/useUpdateApp.ts b/frontend/src/hooks/useUpdateApp.ts new file mode 100644 index 00000000..a362676c --- /dev/null +++ b/frontend/src/hooks/useUpdateApp.ts @@ -0,0 +1,39 @@ +import { useEffect, useState } from 'react'; +import * as Updates from 'expo-updates'; + +export function useUpdateApp() { + const [loading, setLoading] = useState(false); + + const checkUpdate = async () => { + const { isAvailable } = await Updates.checkForUpdateAsync(); + if (isAvailable) { + setLoading(true); + performUpdate(); + } + }; + + const performUpdate = async () => { + Updates.fetchUpdateAsync() + .then(() => { + reloadApp(); + }) + .catch(() => { + reloadApp(); + }); + }; + + const reloadApp = async () => { + await Updates.reloadAsync(); + }; + + useEffect(() => { + if (!__DEV__) { + checkUpdate(); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return { + loading, + }; +} diff --git a/frontend/src/icons.ts b/frontend/src/icons.ts index 8341e930..f57a01cb 100644 --- a/frontend/src/icons.ts +++ b/frontend/src/icons.ts @@ -4,6 +4,7 @@ import Admin from '../assets/icons/AdminPanel.svg'; import AlternateEmail from '../assets/icons/AlternateEmail.svg'; import ArrowUpward from '../assets/icons/ArrowUpward.svg'; import Ballot from '../assets/icons/Ballot.svg'; +import BulletList from '../assets/icons/BulletList.svg'; import Cancel from '../assets/icons/Cancel.svg'; import Chart from '../assets/icons/Chart.svg'; import CheckCircle from '../assets/icons/CheckCircle.svg'; @@ -26,6 +27,7 @@ import More from '../assets/icons/More.svg'; import MoreVert from '../assets/icons/MoreVert.svg'; import NotificationActive from '../assets/icons/NotificationActive.svg'; import Notifications from '../assets/icons/Notifications.svg'; +import NumberList from '../assets/icons/NumberList.svg'; import Person from '../assets/icons/Person.svg'; import Photo from '../assets/icons/Photo.svg'; import PoliceBadge from '../assets/icons/PoliceBadge.svg'; @@ -43,6 +45,9 @@ import Unreachable from '../assets/icons/Unreachable.svg'; import Edit from '../assets/icons/Edit.svg'; import WarningCircle from '../assets/icons/WarningCircle.svg'; import Delete from '../assets/icons/Delete.svg'; +import BoldText from '../assets/icons/BoldText.svg'; +import ItalicText from '../assets/icons/ItalicText.svg'; +import QuoteText from '../assets/icons/QuoteText.svg'; export const Icons = { Add, @@ -51,6 +56,7 @@ export const Icons = { AlternateEmail, ArrowUpward, Ballot, + BulletList, Cancel, Chart, CheckCircle, @@ -73,6 +79,7 @@ export const Icons = { MoreVert, NotificationActive, Notifications, + NumberList, Person, Photo, PoliceBadge, @@ -90,6 +97,9 @@ export const Icons = { Edit, WarningCircle, Delete, + BoldText, + ItalicText, + QuoteText, }; export type IconName = keyof typeof Icons; diff --git a/frontend/src/navigation/AppNavigator.tsx b/frontend/src/navigation/AppNavigator.tsx index a6d07fa3..98e89677 100644 --- a/frontend/src/navigation/AppNavigator.tsx +++ b/frontend/src/navigation/AppNavigator.tsx @@ -9,21 +9,25 @@ import { import { StatusBar } from 'expo-status-bar'; import * as Linking from 'expo-linking'; import * as Notifications from 'expo-notifications'; +import { View } from 'react-native'; -import { useColorScheme } from '../theme'; +import { useColorScheme, makeStyles } from '../theme'; import { RootStackParamList } from '../types'; import { DEEP_LINK_SCREEN_CONFIG, EXPO_PREFIX, handleUrl, - isPostOrMessageDetail, onSubscribe, + isEmailLoginOrActivateAccount, + isRouteAvailable, + DeepRoutes, } from '../constants'; import { isRouteBesidePost, postOrMessageDetailPathToRoutes } from '../helpers'; import { useRedirect } from '../utils'; import { useInitialLoad } from '../hooks/useInitialLoad'; import { LoadingOrErrorView } from '../components'; import { useAuth } from '../utils/AuthProvider'; +import { useUpdateApp } from '../hooks/useUpdateApp'; import RootStackNavigator from './RootStackNavigator'; import { navigationRef } from './NavigationService'; @@ -33,26 +37,30 @@ export default function AppNavigator() { const useInitialLoadResult = useInitialLoad(); const { setRedirectPath } = useRedirect(); const auth = useAuth(); + const styles = useStyles(); + const { loading: appUpdateLoading } = useUpdateApp(); const darkMode = colorScheme === 'dark'; return ( <> - {useInitialLoadResult.loading || auth.isLoading ? ( - + {useInitialLoadResult.loading || auth.isLoading || appUpdateLoading ? ( + ) : ( - - - + + + + + )} ); @@ -97,10 +105,24 @@ const createLinkingConfig = (params: CreateLinkingConfigParams) => { const routeToLogin = { routes: [{ name: 'Login' }] }; // If we're not on a known deep link path, fallback to the default behavior // from React Navigation. - if (!isPostOrMessageDetail(route)) { + if (!isRouteAvailable(route)) { return getStateFromPath(fullPath, config); } + if (isEmailLoginOrActivateAccount(route)) { + return { + routes: [ + { + name: 'Login', + params: { + emailToken: pathParams[0], + isActivateAccount: route === DeepRoutes['activate-account'], + }, + }, + ], + }; + } + if (!isLoggedIn) { setRedirectPath(pathname); /** @@ -125,3 +147,10 @@ const createLinkingConfig = (params: CreateLinkingConfigParams) => { }; return linking; }; + +const useStyles = makeStyles(({ colors }) => ({ + background: { + flex: 1, + backgroundColor: colors.background, + }, +})); diff --git a/frontend/src/navigation/RootStackNavigator.tsx b/frontend/src/navigation/RootStackNavigator.tsx index 39a776dc..5e812d32 100644 --- a/frontend/src/navigation/RootStackNavigator.tsx +++ b/frontend/src/navigation/RootStackNavigator.tsx @@ -58,6 +58,7 @@ export default function RootStackNavigator(props: RootStackNavigatorProps) { screenOptions={{ ...navHeader, presentation: 'card', + headerBackTestID: 'HeaderBackButton', }} > { @@ -70,6 +71,10 @@ export default function RootStackNavigator(props: RootStackNavigatorProps) { name="Login" component={Login} options={{ title: '' }} + initialParams={{ + emailToken: undefined, + isActivateAccount: undefined, + }} /> ) : /** * second condition is used for public discourse where you can access home scene but not profile scene in tab if not login diff --git a/frontend/src/navigation/TabNavigator.tsx b/frontend/src/navigation/TabNavigator.tsx index 09756f00..5e8389b3 100644 --- a/frontend/src/navigation/TabNavigator.tsx +++ b/frontend/src/navigation/TabNavigator.tsx @@ -7,10 +7,10 @@ import { } from '@react-navigation/bottom-tabs'; import { Icon, Text } from '../core-ui'; -import { getToken } from '../helpers'; import { Home as HomeScene, Profile as ProfileScene } from '../screens'; import { makeStyles, useTheme } from '../theme'; import { TabParamList } from '../types'; +import { useAuth } from '../utils/AuthProvider'; const Tab = createBottomTabNavigator(); @@ -18,12 +18,13 @@ function TabBar({ state, navigation: { navigate } }: BottomTabBarProps) { const insets = useSafeAreaInsets(); const styles = useStyles(); const { colors } = useTheme(); + const useAuthResults = useAuth(); return ( {state.routes.map((route: { name: string }, index: number) => { const onPress = async () => { - const token = await getToken(); + const token = !useAuthResults.isLoading && useAuthResults.token; if (state.index === 0 && state.index === index) { navigate(route.name, { backToTop: true }); } else { @@ -41,6 +42,7 @@ function TabBar({ state, navigation: { navigate } }: BottomTabBarProps) { onPress={onPress} style={styles.tab} activeOpacity={state.index === index ? 1 : 0.2} + testID={route.name === 'Profile' ? 'Tab:Profile' : 'Tab:Home'} > { + return withAndroidManifest(config, async (config) => { + config.modResults = await setFirebaseMessagingConfig( + config, + config.modResults, + ); + return config; + }); +}; + +async function setFirebaseMessagingConfig(config, androidManifest) { + // Ensure that the manifest has an application node + const mainApplication = + AndroidConfig.Manifest.getMainApplication(androidManifest); + + // Add the custom meta-data + AndroidConfig.Manifest.addMetaDataItemToMainApplication( + mainApplication, + // value for android:name + 'com.google.firebase.messaging.default_notification_icon', + // value for android:resource + '@drawable/notification_icon', + 'resource', + ); + + return androidManifest; +} + +module.exports = withFirebaseMessagingNotificationIcon; diff --git a/frontend/src/reactiveVars/tokenReactive.mock.tsx b/frontend/src/reactiveVars/tokenReactive.mock.tsx new file mode 100644 index 00000000..3bc9cf0a --- /dev/null +++ b/frontend/src/reactiveVars/tokenReactive.mock.tsx @@ -0,0 +1,25 @@ +import { makeVar } from '@apollo/client'; + +/** + * We use reactive variable for token state instead of useState because we need to be able to update token state from apollo link + * Token state is undefined when we are still loading token from storage and null when we are not logged in. + */ +const tokenVar = makeVar('123456789'); + +/** + * Logout token hold the last token we have before user logged out + * We need this to be able to logout user even when token state is already null + */ +const logoutTokenVar = makeVar('123456789'); + +/** + * Update token state when successfully logged in + */ +const setTokenState = (token: string | null) => { + tokenVar(token); + if (token) { + logoutTokenVar(token); + } +}; + +export { tokenVar, logoutTokenVar, setTokenState }; diff --git a/frontend/src/screens/EditProfile.tsx b/frontend/src/screens/EditProfile.tsx index 31ed180f..ae42728a 100644 --- a/frontend/src/screens/EditProfile.tsx +++ b/frontend/src/screens/EditProfile.tsx @@ -335,15 +335,7 @@ export default function EditProfile(props: ProfileProps) { enabled keyboardVerticalOffset={90} > - { - // if (scrollViewRef.current && show) { - // scrollViewRef.current.scrollToEnd({ animated: true }); - // } - // }} - > + )} /> @@ -433,6 +426,7 @@ export default function EditProfile(props: ProfileProps) { returnKeyType="next" autoCapitalize="words" style={styles.spacingBottom} + testID="EditProfile:TextInput:Name" /> )} /> diff --git a/frontend/src/screens/EditUserStatus/EditUserStatus.tsx b/frontend/src/screens/EditUserStatus/EditUserStatus.tsx index b1698090..b906d511 100644 --- a/frontend/src/screens/EditUserStatus/EditUserStatus.tsx +++ b/frontend/src/screens/EditUserStatus/EditUserStatus.tsx @@ -165,7 +165,10 @@ export default function EditUserStatus() { {emojiText ? ( - + {emojiText} @@ -176,6 +179,7 @@ export default function EditUserStatus() { emojiCode={emojiCode} style={styles.emojiStatus} onPress={navEmojiPicker} + testIDButton="EditUserStatus:Button:Emoji" /> )} @@ -188,7 +192,11 @@ export default function EditUserStatus() { defaultValue={paramStatus} rules={{ required: true }} render={({ field: { onChange, value } }) => ( - + )} /> @@ -253,6 +261,7 @@ export default function EditUserStatus() { }} disabled={!paramStatus} loading={isLoading} + testID="EditUserStatus:Button:DeleteStatus" /> diff --git a/frontend/src/screens/EmojiPicker.tsx b/frontend/src/screens/EmojiPicker.tsx index d805824a..501a99fa 100644 --- a/frontend/src/screens/EmojiPicker.tsx +++ b/frontend/src/screens/EmojiPicker.tsx @@ -72,6 +72,7 @@ export default function EmojiPicker() { navigate('EditUserStatus', { emojiCode: name, emojiText: emoji }); }} key={`emoji-${name}`} + testID={`EmojiPicker:Button:Emoji:${name}`} > {emoji} @@ -91,6 +92,7 @@ export default function EmojiPicker() { }} placeholder={t('Search for ...')} style={styles.textInput} + testID="EmojiPicker:TextInput:Search" /> {/* using flash list because it more faster than flatlist in this case to render more than 3000 emoji */} diff --git a/frontend/src/screens/Home/Home.tsx b/frontend/src/screens/Home/Home.tsx index 887dc46b..3b83e05f 100644 --- a/frontend/src/screens/Home/Home.tsx +++ b/frontend/src/screens/Home/Home.tsx @@ -15,6 +15,7 @@ import Animated, { useAnimatedStyle, useSharedValue, } from 'react-native-reanimated'; +import { useFormContext } from 'react-hook-form'; import { FooterLoadingIndicator, @@ -41,7 +42,6 @@ import { clamp, errorHandler, errorHandlerAlert, - isFlatList, LoginError, transformTopicToPost, useStorage, @@ -99,6 +99,7 @@ type SortOption = typeof sortOptionsArray[number]; export default function Home() { const { refetch: siteRefetch } = useSiteSettings(); const styles = useStyles(); + const { setValue } = useFormContext(); const tabNavigation = useNavigation>(); const { addListener, navigate } = useNavigation>(); @@ -272,10 +273,7 @@ export default function Home() { }, [setData, topicDataList]); useLayoutEffect(() => { - if (!isFlatList(postListRef.current)) { - return; - } - postListRef.current.scrollToIndex({ + postListRef.current?.scrollToIndex({ index: 0, viewOffset: headerViewHeight, }); @@ -335,8 +333,8 @@ export default function Home() { ]); const postListRef = useRef>(null); - if (routeParams && isFlatList(postListRef.current)) { - postListRef.current.scrollToIndex({ + if (routeParams) { + postListRef.current?.scrollToIndex({ index: 0, viewOffset: headerViewHeight, }); @@ -349,6 +347,12 @@ export default function Home() { const onPressAdd = () => { const currentUserId = storage.getItem('user')?.id; if (currentUserId) { + /** + * Set the channel ID in a new post to use the same channel as the home channel. + */ + setValue('channelId', storage.getItem('homeChannelId'), { + shouldDirty: true, + }); navigate('NewPost'); } else { errorHandlerAlert(LoginError, navigate); @@ -494,39 +498,38 @@ export default function Home() { return ; } return ( - <> - { - return ( - - ); - }} - ListFooterComponent={ - - } - /> - + { + return ( + + ); + }} + ListFooterComponent={ + + } + testID="Home:PostList" + /> ); }; @@ -549,6 +552,7 @@ export default function Home() { - onPressTitle()}> + {title} - {ios && } + {ios && ( + + )} ); } diff --git a/frontend/src/screens/Hyperlink.tsx b/frontend/src/screens/Hyperlink.tsx index 81cc7a17..4e4046df 100644 --- a/frontend/src/screens/Hyperlink.tsx +++ b/frontend/src/screens/Hyperlink.tsx @@ -106,6 +106,7 @@ export default function Hyperlink() { onSubmitEditing={() => titleInputRef.current?.focus()} autoCapitalize="none" style={styles.spacingBottom} + testID="Hyperlink:TextInput:URL" /> )} /> @@ -124,6 +125,7 @@ export default function Hyperlink() { onBlur={onBlur} autoCapitalize="none" style={styles.spacingBottom} + testID="Hyperlink:TextInput:Title" /> )} /> diff --git a/frontend/src/screens/Login.tsx b/frontend/src/screens/Login.tsx index d8b73988..59885bbb 100644 --- a/frontend/src/screens/Login.tsx +++ b/frontend/src/screens/Login.tsx @@ -1,18 +1,38 @@ -import React, { useRef, useState } from 'react'; -import { Image, Keyboard, View } from 'react-native'; +import React, { useEffect, useRef, useState } from 'react'; +import { Image, Keyboard, View, Platform, Alert } from 'react-native'; import { KeyboardAwareScrollView } from 'react-native-keyboard-aware-scroll-view'; -import { useNavigation } from '@react-navigation/native'; +import { useNavigation, useRoute } from '@react-navigation/native'; import { Controller, useForm } from 'react-hook-form'; +import * as AppleAuthentication from 'expo-apple-authentication'; import { DarkLogo, LightLogo } from '../../assets/images'; import { CustomHeader } from '../components'; -import { Button, Text, TextInput, TextInputType } from '../core-ui'; +import { + Button, + Divider, + Text, + TextInput, + TextInputType, + AppleSignInButton, + RadioButton, +} from '../core-ui'; import { errorHandler, getImage, useStorage } from '../helpers'; -import { useLogin, usePushNotificationsToken, useSiteSettings } from '../hooks'; +import { + useActivateAccount, + useAuthenticateLoginLink, + useLogin, + useLoginWithApple, + usePushNotificationsToken, + useRequestLoginLink, + useSiteSettings, +} from '../hooks'; import { makeStyles, useColorScheme } from '../theme'; -import { StackNavProp } from '../types'; +import { ASAuthorizationError, StackNavProp, StackRouteProp } from '../types'; import { useRedirect } from '../utils'; import { useAuth } from '../utils/AuthProvider'; +import { usePluginStatus } from '../hooks/site/usePluginStatus'; +import { LoginOutputFragment } from '../generated/server'; +import { ERROR_UNEXPECTED, LOGIN_LINK_SUCCESS_ALERT } from '../constants'; type Form = { email: string; @@ -24,42 +44,94 @@ let tempUser = { email: '', password: '' }; export default function Login() { const { colorScheme } = useColorScheme(); const { canSignUp = false } = useSiteSettings(); + const { appleLoginEnabled, loginLinkEnabled } = usePluginStatus(); const storage = useStorage(); const { setTokenState } = useAuth(); const styles = useStyles(); const { redirectPath, setRedirectPath, handleRedirect } = useRedirect(); + const { params } = useRoute>(); const [hidePassword, setHidePassword] = useState(true); + const [errorMsg, setErrorMsg] = useState(''); + const [loginWithLink, setLoginWithLink] = useState(false); const { navigate } = useNavigation>(); const { syncToken } = usePushNotificationsToken(); - const { login, loading, error } = useLogin({ - onCompleted: async ({ login: authUser }) => { + const { requestLoginLink } = useRequestLoginLink({ + onCompleted: ({ requestLoginLink }) => { + if (requestLoginLink === 'success') { + Alert.alert( + t('Success!'), + LOGIN_LINK_SUCCESS_ALERT, + [{ text: 'Got it' }], + { cancelable: false }, + ); + } + }, + }); + + const ios = Platform.OS === 'ios'; + + const onLoginCompleted = async (data: LoginOutputFragment) => { + setTokenState(data.token); + let { user, enableLexiconPushNotifications } = data; + storage.setItem('user', { + id: user.id, + username: user.username, + name: user.name ?? '', + avatar: getImage(user.avatar), + trustLevel: user.trustLevel, + groups: user.groups.map((group) => group.name), + }); + + if (enableLexiconPushNotifications) { + syncToken(); + } + if (redirectPath) { + handleRedirect(); + setRedirectPath(''); + } + }; + const { login, loading: loginLoading } = useLogin({ + onCompleted: ({ login: authUser }) => { // eslint-disable-next-line no-underscore-dangle if (authUser.__typename === 'LoginOutput') { - setTokenState(authUser.token); - let { user } = authUser; - storage.setItem('user', { - id: user.id, - username: user.username, - name: user.name ?? '', - avatar: getImage(user.avatar), - trustLevel: user.trustLevel, - groups: user.groups.map((group) => group.name), - }); - syncToken(); - - if (redirectPath) { - handleRedirect(); - setRedirectPath(''); - } + onLoginCompleted(authUser); // eslint-disable-next-line no-underscore-dangle } else if (authUser.__typename === 'SecondFactorRequired') { navigate('TwoFactorAuth', tempUser); } }, - onError: () => {}, + onError: (error) => { + setErrorMsg(errorHandler(error)); + }, }); + const { loginWithApple, loading: loginWithAppleLoading } = useLoginWithApple({ + onCompleted: ({ loginWithApple }) => { + onLoginCompleted(loginWithApple); + }, + onError: (error) => { + setErrorMsg(errorHandler(error)); + }, + }); + const { activateAccount, loading: activateAccountLoading } = + useActivateAccount({ + onCompleted: ({ activateAccount }) => { + Alert.alert( + t('Success!'), + t('Your new account is confirmed'), + [{ text: 'Got it' }], + { cancelable: false }, + ); + onLoginCompleted(activateAccount); + }, + }); + const { authenticateLoginLink, loading: loginLinkLoading } = + useAuthenticateLoginLink({ + onCompleted: ({ authenticateLoginLink }) => { + onLoginCompleted(authenticateLoginLink); + }, + }); const passwordInputRef = useRef(null); @@ -68,6 +140,7 @@ export default function Login() { handleSubmit, formState: { errors }, formState, + trigger, } = useForm

({ mode: 'onChange', }); @@ -80,15 +153,29 @@ export default function Login() { const onSubmit = handleSubmit(async ({ email, password }) => { Keyboard.dismiss(); - tempUser = { email, password }; - login({ - variables: { - email, - password, - }, - }); + if (loginWithLink) { + requestLoginLink({ variables: { login: email } }); + } else { + tempUser = { email, password }; + login({ + variables: { + email, + password, + }, + }); + } }); + useEffect(() => { + if (params && params.emailToken) { + if (params.isActivateAccount) { + activateAccount({ variables: { token: params.emailToken } }); + } else { + authenticateLoginLink({ variables: { token: params.emailToken } }); + } + } + }, [activateAccount, authenticateLoginLink, params]); + return ( - {error && ( + {errorMsg && ( - {errorHandler(error)} + {errorMsg} )} ( )} /> - ( - setHidePassword(!hidePassword)} - /> - )} - /> + {loginLinkEnabled && ( + { + setLoginWithLink(!loginWithLink); + trigger(); + }} + disabled={false} + style={styles.radioButton} + checkCircleIcon={true} + > + {t('Send login link, skip password')} + + )} + {!loginWithLink && ( + ( + setHidePassword(!hidePassword)} + /> + )} + /> + )}