mirror of
https://github.com/Novaloop-AG/element-x-android.git
synced 2026-10-31 16:22:49 +00:00
Merge branch 'develop' into renovate/org.sonarqube-7.x
This commit is contained in:
+15
-9
@@ -13,18 +13,12 @@
|
||||
"hostRules": [
|
||||
{
|
||||
"matchHost": "https://repo1.maven.org/maven2/",
|
||||
"concurrentRequestLimit": 4,
|
||||
"maxRequestsPerSecond": 4,
|
||||
"concurrentRequestLimit": 10,
|
||||
"maxRequestsPerSecond": 10,
|
||||
"timeout": 10000,
|
||||
}
|
||||
],
|
||||
"packageRules": [
|
||||
{
|
||||
"matchPackageNames": [
|
||||
"/^androidx.\.*/"
|
||||
],
|
||||
"groupName": "androidx",
|
||||
"registryUrls": [ "https://dl.google.com/android/maven2/" ],
|
||||
},
|
||||
{
|
||||
"matchPackageNames": [
|
||||
"org.matrix.rustcomponents:sdk-android",
|
||||
@@ -33,6 +27,18 @@
|
||||
"minimumReleaseAge": null,
|
||||
// Max default value is 6, with this we give absolute priority to our own dependencies
|
||||
"prPriority": 7,
|
||||
"registryUrls": [ "https://repo1.maven.org/maven2/" ],
|
||||
},
|
||||
{
|
||||
// Fetch Android dependencies from Google's Maven repo instead of Maven Central.
|
||||
"matchPackageNames": [
|
||||
"/^androidx\\./",
|
||||
"/^android\\./",
|
||||
"/^com\\.android\\.tools/",
|
||||
"/^com\\.google\\.firebase/",
|
||||
"/^com\\.google\\.android/",
|
||||
],
|
||||
"registryUrls": [ "https://dl.google.com/android/maven2/" ],
|
||||
},
|
||||
{
|
||||
"groupName": "kotlin",
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
name: Danger CI
|
||||
|
||||
on: [pull_request, merge_group]
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
name: Danger main check
|
||||
# Skip in forks, it doesn't work even with the fallback token
|
||||
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Add SSH private keys for submodule repositories
|
||||
uses: webfactory/ssh-agent@e83874834305fe9a4a2997156cb26c5de65a8555 # v0.10.0
|
||||
with:
|
||||
ssh-private-key: ${{ secrets.ELEMENT_ENTERPRISE_DEPLOY_KEY }}
|
||||
- name: Clone submodules
|
||||
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
|
||||
run: git submodule update --init --recursive
|
||||
- run: |
|
||||
npm install --save-dev @babel/plugin-transform-flow-strip-types
|
||||
- name: Danger
|
||||
uses: danger/danger-js@a236e3b8c41f0babbf133568db851a20a34f9a7b # 14.0.5
|
||||
with:
|
||||
args: "--dangerfile ./tools/danger/dangerfile.js"
|
||||
env:
|
||||
DANGER_GITHUB_API_TOKEN: ${{ secrets.DANGER_GITHUB_API_TOKEN }}
|
||||
# Fallback for forks
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -0,0 +1,126 @@
|
||||
name: PR Checks
|
||||
|
||||
on:
|
||||
# Privilege escalation necessary to comment on pull requests from forks.
|
||||
# 🚨 We must not check out or execute any code here, and be careful around use of user-controlled inputs.
|
||||
pull_request_target: # zizmor: ignore[dangerous-triggers]
|
||||
types: [ opened, reopened, edited, synchronize, labeled, unlabeled ]
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
pr-checks:
|
||||
name: PR Checks
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
|
||||
concurrency:
|
||||
# Only allow a single run of this workflow on each branch, automatically cancelling older runs.
|
||||
group: ${{ format('pr-checks-{0}', github.ref) }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
# Required to comment on the pull request.
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- name: Check the pull request
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
script: |
|
||||
// Classes that must not be modified anymore. Add the file name suffix here to freeze a class.
|
||||
const FROZEN_CLASSES = [
|
||||
];
|
||||
|
||||
// Only this user is allowed to edit the translation files, since they come from Localazy.
|
||||
const TRANSLATION_ALLOW_LIST = ["ElementBot"];
|
||||
|
||||
const pr = context.payload.pull_request;
|
||||
const body = pr.body ?? "";
|
||||
|
||||
const files = await github.paginate(github.rest.pulls.listFiles, {
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
pull_number: pr.number
|
||||
});
|
||||
// Deleted files are not interesting for any of the checks below.
|
||||
const paths = files.filter(file => file.status !== "removed").map(file => file.filename);
|
||||
|
||||
const warnings = [];
|
||||
const failures = [];
|
||||
|
||||
if (!body) {
|
||||
warnings.push("Please provide a description for this PR.");
|
||||
}
|
||||
|
||||
if (paths.length > 50) {
|
||||
warnings.push("This pull request seems relatively large. Please consider splitting it into multiple smaller ones.");
|
||||
}
|
||||
|
||||
if (pr.title.endsWith("…")) {
|
||||
failures.push("Please provide a complete title that can be used as a changelog entry.");
|
||||
}
|
||||
|
||||
if (pr.labels.filter(label => label.name.startsWith("PR-")).length !== 1) {
|
||||
failures.push("Please add a single `PR-` label to categorise the changelog entry.");
|
||||
}
|
||||
|
||||
for (const frozen of FROZEN_CLASSES) {
|
||||
if (paths.some(path => path.endsWith(frozen))) {
|
||||
failures.push(`Frozen class \`${frozen}\` has been modified. Please do not modify frozen class.`);
|
||||
}
|
||||
}
|
||||
|
||||
// Exclude the recorded screenshots, which are legitimately pngs.
|
||||
if (paths.some(path => path.toLowerCase().endsWith(".png") && !path.includes("snapshots/images/"))) {
|
||||
warnings.push("You seem to have made changes to some images. Please consider using a vector drawable.");
|
||||
}
|
||||
|
||||
if (!TRANSLATION_ALLOW_LIST.includes(pr.user.login) && paths.some(path => path.endsWith("translations.xml"))) {
|
||||
failures.push("Some translation files have been edited. Only user `ElementBot` (i.e. translations coming from Localazy) is allowed to do that. " +
|
||||
"Please read more about translations management [in the doc](https://github.com/element-hq/element-x-android/blob/develop/CONTRIBUTING.md#strings).");
|
||||
}
|
||||
|
||||
warnings.forEach(warning => core.warning(warning));
|
||||
failures.forEach(failure => core.error(failure));
|
||||
|
||||
// Keep a single comment up to date rather than adding a new one on every run.
|
||||
const marker = "<!-- pr-checks -->";
|
||||
const comments = await github.paginate(github.rest.issues.listComments, {
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: pr.number
|
||||
});
|
||||
const existing = comments.find(comment => comment.body.includes(marker));
|
||||
|
||||
if (warnings.length || failures.length) {
|
||||
const lines = [...failures.map(failure => `- ❌ ${failure}`),
|
||||
...warnings.map(warning => `- ⚠️ ${warning}`)];
|
||||
const commentBody = `${marker}\n### PR Checks\n\n${lines.join("\n")}`;
|
||||
|
||||
if (existing) {
|
||||
await github.rest.issues.updateComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
comment_id: existing.id,
|
||||
body: commentBody
|
||||
});
|
||||
} else {
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: pr.number,
|
||||
body: commentBody
|
||||
});
|
||||
}
|
||||
} else if (existing) {
|
||||
await github.rest.issues.deleteComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
comment_id: existing.id
|
||||
});
|
||||
}
|
||||
|
||||
if (failures.length) {
|
||||
core.setFailed(`${failures.length} check(s) failed, see the comment on the pull request.`);
|
||||
}
|
||||
+159
-21
@@ -342,28 +342,166 @@ jobs:
|
||||
name: Project Check Suite
|
||||
runs-on: ubuntu-latest
|
||||
needs: [konsist, lint, ktlint, detekt]
|
||||
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
|
||||
# Run even if a previous job failed: that is precisely when there are issues to report.
|
||||
if: ${{ !cancelled() }}
|
||||
permissions:
|
||||
# Required to list the files modified by the pull request.
|
||||
pull-requests: read
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
# Ensure we are building the branch and not the branch after being merged on develop
|
||||
# https://github.com/actions/checkout/issues/881
|
||||
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }}
|
||||
persist-credentials: false
|
||||
- name: Download reports from previous jobs
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
- name: Prepare Danger
|
||||
if: always()
|
||||
run: |
|
||||
npm install --save-dev @babel/core
|
||||
npm install --save-dev @babel/plugin-transform-flow-strip-types
|
||||
yarn add danger-plugin-lint-report --dev
|
||||
- name: Danger lint
|
||||
if: always()
|
||||
uses: danger/danger-js@a236e3b8c41f0babbf133568db851a20a34f9a7b # 14.0.5
|
||||
- name: Annotate the lint, ktlint and detekt issues
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
args: "--dangerfile ./tools/danger/dangerfile-lint.js"
|
||||
env:
|
||||
DANGER_GITHUB_API_TOKEN: ${{ secrets.DANGER_GITHUB_API_TOKEN }}
|
||||
# Fallback for forks
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
// GitHub only surfaces a handful of annotations per level and per step, so cap what we emit
|
||||
// and let the job summary carry the full list.
|
||||
const MAX_ANNOTATIONS = 20;
|
||||
const MAX_SUMMARY_ROWS = 200;
|
||||
|
||||
function unescapeXml(value) {
|
||||
return value
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, "\"")
|
||||
.replace(/'/g, "'")
|
||||
.replace(/&#(\d+);/g, (_, code) => String.fromCharCode(Number(code)))
|
||||
.replace(/&/g, "&");
|
||||
}
|
||||
|
||||
function attribute(tag, name) {
|
||||
const match = new RegExp(`\\b${name}="([^"]*)"`).exec(tag);
|
||||
return match ? unescapeXml(match[1]) : undefined;
|
||||
}
|
||||
|
||||
// Paths in the reports are absolute paths on the runner which generated them, i.e.
|
||||
// /home/runner/work/<repo>/<repo>/<path>.
|
||||
const workspace = `/work/${context.repo.repo}/${context.repo.repo}/`;
|
||||
function repoRelative(path) {
|
||||
const index = path.indexOf(workspace);
|
||||
return index === -1 ? path : path.slice(index + workspace.length);
|
||||
}
|
||||
|
||||
// Checkstyle XML, as produced by ktlint and detekt:
|
||||
// <file name="…"><error line=".." column=".." severity=".." message=".." source=".."/></file>
|
||||
function parseCheckstyle(content) {
|
||||
const issues = [];
|
||||
const fileRegex = /<file\s+([^>]*?)(\/>|>([\s\S]*?)<\/file>)/g;
|
||||
let file;
|
||||
while ((file = fileRegex.exec(content)) !== null) {
|
||||
const path = attribute(file[1], "name");
|
||||
if (!path) continue;
|
||||
const errorRegex = /<error\s+([^>]*?)\/?>/g;
|
||||
let error;
|
||||
while ((error = errorRegex.exec(file[3] ?? "")) !== null) {
|
||||
issues.push({
|
||||
file: repoRelative(path),
|
||||
line: Number(attribute(error[1], "line")) || 1,
|
||||
column: Number(attribute(error[1], "column")) || undefined,
|
||||
severity: (attribute(error[1], "severity") ?? "warning").toLowerCase(),
|
||||
rule: attribute(error[1], "source"),
|
||||
message: attribute(error[1], "message") ?? "",
|
||||
});
|
||||
}
|
||||
}
|
||||
return issues;
|
||||
}
|
||||
|
||||
// Android lint XML, which is not Checkstyle:
|
||||
// <issue id=".." severity=".." message=".."><location file=".." line=".." column=".."/></issue>
|
||||
function parseAndroidLint(content) {
|
||||
const issues = [];
|
||||
const issueRegex = /<issue\s+([^>]*?)(\/>|>([\s\S]*?)<\/issue>)/g;
|
||||
let issue;
|
||||
while ((issue = issueRegex.exec(content)) !== null) {
|
||||
const location = /<location\s+([^>]*?)\/?>/.exec(issue[3] ?? "");
|
||||
if (!location) continue;
|
||||
const path = attribute(location[1], "file");
|
||||
if (!path) continue;
|
||||
issues.push({
|
||||
file: repoRelative(path),
|
||||
line: Number(attribute(location[1], "line")) || 1,
|
||||
column: Number(attribute(location[1], "column")) || undefined,
|
||||
severity: (attribute(issue[1], "severity") ?? "warning").toLowerCase(),
|
||||
rule: attribute(issue[1], "id"),
|
||||
message: attribute(issue[1], "message") ?? "",
|
||||
});
|
||||
}
|
||||
return issues;
|
||||
}
|
||||
|
||||
const reports = await (await glob.create("**/reports/**/*.xml")).glob();
|
||||
core.info(`Found ${reports.length} XML report(s).`);
|
||||
|
||||
const fs = require("fs");
|
||||
let issues = [];
|
||||
for (const report of reports) {
|
||||
const content = fs.readFileSync(report, "utf8");
|
||||
if (content.includes("<checkstyle")) {
|
||||
issues.push(...parseCheckstyle(content));
|
||||
} else if (content.includes("<issues")) {
|
||||
issues.push(...parseAndroidLint(content));
|
||||
}
|
||||
}
|
||||
|
||||
// Only report on the files touched by the pull request.
|
||||
const pr = context.payload.pull_request;
|
||||
if (pr) {
|
||||
const files = await github.paginate(github.rest.pulls.listFiles, {
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
pull_number: pr.number
|
||||
});
|
||||
const changed = new Set(files.map(file => file.filename));
|
||||
issues = issues.filter(issue => changed.has(issue.file));
|
||||
}
|
||||
|
||||
const seen = new Set();
|
||||
issues = issues.filter(issue => {
|
||||
const key = `${issue.file}:${issue.line}:${issue.rule}:${issue.message}`;
|
||||
if (seen.has(key)) return false;
|
||||
seen.add(key);
|
||||
return true;
|
||||
});
|
||||
|
||||
if (issues.length === 0) {
|
||||
core.info("No issue found in the files modified by this pull request.");
|
||||
return;
|
||||
}
|
||||
|
||||
const level = severity => {
|
||||
if (severity === "error" || severity === "fatal") return "error";
|
||||
if (severity === "warning") return "warning";
|
||||
return "notice";
|
||||
};
|
||||
const rank = { error: 0, warning: 1, notice: 2 };
|
||||
issues.sort((a, b) =>
|
||||
rank[level(a.severity)] - rank[level(b.severity)] ||
|
||||
a.file.localeCompare(b.file) ||
|
||||
a.line - b.line
|
||||
);
|
||||
|
||||
issues.slice(0, MAX_ANNOTATIONS).forEach(issue => {
|
||||
const annotation = {
|
||||
title: issue.rule,
|
||||
file: issue.file,
|
||||
startLine: issue.line,
|
||||
startColumn: issue.column,
|
||||
};
|
||||
core[level(issue.severity)](issue.message, annotation);
|
||||
});
|
||||
|
||||
core.summary.addHeading(`${issues.length} code quality issue(s)`, 3);
|
||||
core.summary.addTable([
|
||||
[{ data: "Severity", header: true }, { data: "File", header: true }, { data: "Rule", header: true }, { data: "Message", header: true }],
|
||||
...issues.slice(0, MAX_SUMMARY_ROWS).map(issue => [
|
||||
level(issue.severity),
|
||||
`${issue.file}:${issue.line}`,
|
||||
issue.rule ?? "",
|
||||
issue.message,
|
||||
]),
|
||||
]);
|
||||
if (issues.length > MAX_SUMMARY_ROWS) {
|
||||
core.summary.addRaw(`…and ${issues.length - MAX_SUMMARY_ROWS} more, see the uploaded reports.`, true);
|
||||
}
|
||||
await core.summary.write();
|
||||
|
||||
Generated
-1
@@ -16,7 +16,6 @@
|
||||
<w>posthog</w>
|
||||
<w>rageshake</w>
|
||||
<w>securebackup</w>
|
||||
<w>showkase</w>
|
||||
<w>snackbar</w>
|
||||
<w>spdx</w>
|
||||
<w>swipeable</w>
|
||||
|
||||
+60
@@ -1,3 +1,63 @@
|
||||
Changes in Element X v26.09.2
|
||||
=============================
|
||||
|
||||
<!-- Release notes generated using configuration in .github/release.yml at v26.09.2 -->
|
||||
|
||||
## What's Changed
|
||||
### 🙌 Improvements
|
||||
* Keep the push foreground service alive while notifications are fetched by @Johennes in https://github.com/element-hq/element-x-android/pull/7639
|
||||
* Stop clearing the remote config when clearing the cache by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7662
|
||||
### 🐛 Bugfixes
|
||||
* Fix fetched remote config being cached with the wrong key by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7626
|
||||
* Make sure `enforce_element_pro` is taken into account in FOSS by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7631
|
||||
### 🗣 Translations
|
||||
* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/7628
|
||||
* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/7657
|
||||
### 🧱 Build
|
||||
* Allow retrying finding the release assets in release script by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7619
|
||||
* Improve detection of invalid placeholders in localazy strings by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7624
|
||||
* Add `GRADLE_OPTS` for `sync-localazy.yml` flow by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7629
|
||||
* Clean up ProGuard keep rules files by @bmarty in https://github.com/element-hq/element-x-android/pull/7647
|
||||
* Replace Danger with actions/github-script by @bmarty in https://github.com/element-hq/element-x-android/pull/7649
|
||||
* Remove Showkase and its in-app component browser by @bmarty in https://github.com/element-hq/element-x-android/pull/7678
|
||||
* Improve Renovate configuration to fix issues with Maven Central by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7677
|
||||
### Dependency upgrades
|
||||
* Update dependency com.posthog:posthog-android to v3.60.7 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7616
|
||||
* Update dependency org.jsoup:jsoup to v1.23.2 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7620
|
||||
* Update dependency io.element.android:element-call-embedded to v0.25.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7622
|
||||
* Update dependencyAnalysis to v3.19.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7623
|
||||
* Update wysiwyg to v2.42.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7627
|
||||
* Update android.gradle.plugin to v9.3.2 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7603
|
||||
* Update actions/setup-java action to v6 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7614
|
||||
* Update danger/danger-js action to v14 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7602
|
||||
* Update camera to v1.6.2 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7632
|
||||
* Update coil to v3.6.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7630
|
||||
* Update dependency io.sentry:sentry-android to v8.54.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7636
|
||||
* Update dependency org.jetbrains.kotlinx:kotlinx-collections-immutable to v0.5.2 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7650
|
||||
* Bump SDK bindings to `v26.09.3` by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7637
|
||||
* Update zizmorcore/zizmor-action action to v0.6.3 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7658
|
||||
* Update coil to v3.6.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7664
|
||||
* Update dependency org.matrix.rustcomponents:sdk-android to v26.09.08 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7668
|
||||
* Update dependency org.matrix.rustcomponents:sdk-android to v26.09.9 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7683
|
||||
### 💼 Element Pro
|
||||
* Fix remote config getting refreshed with `Origin.Cache` by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7666
|
||||
### Others
|
||||
* Move knock and gallery to labs by @bmarty in https://github.com/element-hq/element-x-android/pull/7625
|
||||
* Say so when this session has no secrets to verify another one with by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7551
|
||||
* Expose the timeline item long press and swipe to reply as TalkBack actions by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7458
|
||||
* Bump PR priority for our dependencies in Renovate by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7642
|
||||
* Import string from Localazy and remove temporary file. by @bmarty in https://github.com/element-hq/element-x-android/pull/7645
|
||||
* Revert "Say so when this session has no secrets to verify another one with" by @bmarty in https://github.com/element-hq/element-x-android/pull/7644
|
||||
* Maestro (local): queue pending runs in the maestro-test concurrency group instead of cancelling them by @andreBurnt in https://github.com/element-hq/element-x-android/pull/7656
|
||||
* Maestro (local): skip the lane on pull requests from forks by @andreBurnt in https://github.com/element-hq/element-x-android/pull/7655
|
||||
* Update maplibre compose to 0.15 by @ganfra in https://github.com/element-hq/element-x-android/pull/7663
|
||||
* Try improving renovate issues with Maven Central rate limiting by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7667
|
||||
|
||||
## New Contributors
|
||||
* @andreBurnt made their first contribution in https://github.com/element-hq/element-x-android/pull/7656
|
||||
|
||||
**Full Changelog**: https://github.com/element-hq/element-x-android/compare/v26.09.1...v26.09.2
|
||||
|
||||
Changes in Element X v26.09.1
|
||||
=============================
|
||||
|
||||
|
||||
+1
-1
@@ -72,7 +72,7 @@ allprojects {
|
||||
verbose = true
|
||||
reporters {
|
||||
reporter(org.jlleitschuh.gradle.ktlint.reporter.ReporterType.PLAIN)
|
||||
// To have XML report for Danger
|
||||
// To have XML report for the CI to annotate the PR, see .github/workflows/quality.yml
|
||||
reporter(org.jlleitschuh.gradle.ktlint.reporter.ReporterType.CHECKSTYLE)
|
||||
}
|
||||
val generatedPath = "${layout.buildDirectory.asFile.get()}/generated/"
|
||||
|
||||
@@ -358,7 +358,8 @@ Some dependency, mainly because they are not shared can be declared in `build.gr
|
||||
We have 3 tests frameworks in place, and this should be sufficient to guarantee a good code coverage and limit regressions hopefully:
|
||||
|
||||
- Maestro to test the global usage of the application. See the related [documentation](../.maestro/README.md).
|
||||
- Combination of [Showkase](https://github.com/airbnb/Showkase) and [Paparazzi](https://github.com/cashapp/paparazzi), to test UI pixel perfect. To add test,
|
||||
- Combination of [Paparazzi](https://github.com/cashapp/paparazzi) and
|
||||
[ComposablePreviewScanner](https://github.com/sergio-sastre/ComposablePreviewScanner), to test UI pixel perfect. To add test,
|
||||
just add `@Preview` for the composable you are adding. See the related [documentation](screenshot_testing.md) and see in the template the
|
||||
file [TemplateView.kt](../features/template/src/main/kotlin/io/element/android/features/template/TemplateView.kt). We create PreviewParam classes to provide
|
||||
different states. See for instance the
|
||||
@@ -449,9 +450,8 @@ number at the bottom 7 times. After tapping, a new "Developer options" entry wil
|
||||
at the bottom of the list.
|
||||
|
||||
The developer options include feature flags, notification/push history, Element call
|
||||
customization, Rust SDK log levels, per-feature tracing toggles, Showkase to debug UI
|
||||
components, rageshake controls, app crash controls, cache details/controls, persistent
|
||||
storage maintenance tasks.
|
||||
customization, Rust SDK log levels, per-feature tracing toggles, rageshake controls, app
|
||||
crash controls, cache details/controls, persistent storage maintenance tasks.
|
||||
|
||||
Keywords: Developer settings, developer mode
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ The CI checks that:
|
||||
2. The tests are passing
|
||||
3. The code quality is good (detekt, ktlint, lint)
|
||||
4. The code is running and smoke tests are passing (maestro)
|
||||
5. The PullRequest itself is good (with danger)
|
||||
5. The PullRequest itself is good (see the [PR Checks workflow](../.github/workflows/pr-checks.yml))
|
||||
6. Files that must be added with git-lfs are added with git-lfs
|
||||
|
||||
## What is the CI reporting
|
||||
|
||||
-105
@@ -1,105 +0,0 @@
|
||||
## Danger
|
||||
|
||||
<!--- TOC -->
|
||||
|
||||
* [What does danger checks](#what-does-danger-checks)
|
||||
* [PR check](#pr-check)
|
||||
* [Quality check](#quality-check)
|
||||
* [Setup](#setup)
|
||||
* [Run danger locally](#run-danger-locally)
|
||||
* [Danger user](#danger-user)
|
||||
* [Useful links](#useful-links)
|
||||
|
||||
<!--- END -->
|
||||
|
||||
## What does danger checks
|
||||
|
||||
### PR check
|
||||
|
||||
See the [dangerfile](../tools/danger/dangerfile.js). If you add rules in the dangerfile, please update the list below!
|
||||
|
||||
Here are the checks that Danger does so far:
|
||||
|
||||
- PR description is not empty
|
||||
- Big PR got a warning to recommend to split
|
||||
- PR contains a correct title and a label to categorize the release note
|
||||
- PR does not modify frozen classes
|
||||
- PR with change on layout should include screenshot in the description (TODO Not supported yet!)
|
||||
- PR which adds png file warn about the usage of vector drawables
|
||||
- non draft PR should have a reviewer
|
||||
- files containing translations are not modified by developers
|
||||
|
||||
### Quality check
|
||||
|
||||
After all the checks that generate checkstyle XML report, such as Ktlint, lint, or Detekt, Danger is run with this [dangerfile](../tools/danger/dangerfile-lint.js), in order to post comments to the PR with the detected error and warnings.
|
||||
|
||||
To run locally, you will have to install the plugin `danger-plugin-lint-report` using:
|
||||
|
||||
```shell
|
||||
yarn add danger-plugin-lint-report --dev
|
||||
```
|
||||
|
||||
## Setup
|
||||
|
||||
This operation should not be necessary, since Danger is already setup for the project.
|
||||
|
||||
To setup danger to the project, run:
|
||||
|
||||
```shell
|
||||
bundle exec danger init
|
||||
```
|
||||
|
||||
## Run danger locally
|
||||
|
||||
When modifying the [dangerfile](../tools/danger/dangerfile.js), you can check it by running Danger locally.
|
||||
|
||||
To run danger locally, install it and run:
|
||||
|
||||
```shell
|
||||
bundle exec danger pr <PR_URL> --dangerfile=./tools/danger/dangerfile.js
|
||||
```
|
||||
|
||||
For instance:
|
||||
|
||||
```shell
|
||||
bundle exec danger pr https://github.com/element-hq/element-android/pull/6637 --dangerfile=./tools/danger/dangerfile.js
|
||||
```
|
||||
|
||||
We may need to create a GitHub token to have less API rate limiting, and then set the env var:
|
||||
|
||||
```shell
|
||||
export DANGER_GITHUB_API_TOKEN='YOUR_TOKEN'
|
||||
```
|
||||
|
||||
Swift and Kotlin (just in case)
|
||||
|
||||
```shell
|
||||
bundle exec danger-swift pr <PR_URL> --dangerfile=./tools/danger/dangerfile.js
|
||||
bundle exec danger-kotlin pr <PR_URL> --dangerfile=./tools/danger/dangerfile.js
|
||||
```
|
||||
|
||||
## Danger user
|
||||
|
||||
To let Danger check all the PRs, including PRs form forks, a GitHub account have been created:
|
||||
- login: ElementBot
|
||||
- password: Stored on Passbolt
|
||||
- GitHub token: A token with limited access has been created and added to the repository https://github.com/element-hq/element-x-android as secret DANGER_GITHUB_API_TOKEN. This token is not saved anywhere else. In case of problem, just delete it and create a new one, then update the secret.
|
||||
|
||||
PRs from forks do not always have access to the secret `secrets.DANGER_GITHUB_API_TOKEN`, so `secrets.GITHUB_TOKEN` is also provided to the job environment. If `secrets.DANGER_GITHUB_API_TOKEN` is available, it will be used, so user `ElementBot` will comment the PR. Else `secrets.GITHUB_TOKEN` will be used, and bot `github-actions` will comment the PR.
|
||||
|
||||
## Useful links
|
||||
|
||||
- https://danger.systems/
|
||||
- https://danger.systems/js/
|
||||
- https://danger.systems/js/guides/getting_started.html
|
||||
- https://danger.systems/js/reference.html
|
||||
- https://github.com/danger/awesome-danger
|
||||
|
||||
Some danger files to get inspired from
|
||||
|
||||
- https://github.com/artsy/emission/blob/master/dangerfile.ts
|
||||
- https://github.com/facebook/react-native/blob/master/bots/dangerfile.js
|
||||
- https://github.com/apollographql/apollo-client/blob/master/config/dangerfile.ts
|
||||
- https://github.com/styleguidist/react-styleguidist/blob/master/dangerfile.js
|
||||
- https://github.com/storybooks/storybook/blob/master/dangerfile.js
|
||||
- https://github.com/ReactiveX/rxjs/blob/master/dangerfile.js
|
||||
+1
-1
Submodule enterprise updated: bb07b4f839...f463003f68
@@ -0,0 +1,2 @@
|
||||
Main changes in this version: bug fixes and improvements.
|
||||
Full changelog: https://github.com/element-hq/element-x-android/releases
|
||||
+19
-2
@@ -13,6 +13,23 @@ import io.element.android.libraries.matrix.api.core.SessionId
|
||||
/**
|
||||
* A hook that can be used to customize the [MatrixClientBuilder] for enterprise features.
|
||||
*/
|
||||
fun interface ClientBuilderEnterpriseHook {
|
||||
suspend operator fun invoke(clientBuilder: MatrixClientBuilder, sessionId: SessionId): MatrixClientBuilder
|
||||
interface ClientBuilderEnterpriseHook {
|
||||
/**
|
||||
* Customize the [MatrixClientBuilder] for enterprise features.
|
||||
* This method is invoked everytime a new [MatrixClientBuilder] is created.
|
||||
*
|
||||
* @param clientBuilder The [MatrixClientBuilder] to customize.
|
||||
* @return The customized [MatrixClientBuilder].
|
||||
*/
|
||||
suspend fun beforeClientCreation(clientBuilder: MatrixClientBuilder): MatrixClientBuilder
|
||||
|
||||
/**
|
||||
* Customize the [MatrixClientBuilder] for enterprise features.
|
||||
* This method is invoked as well as the other method when a new [MatrixClientBuilder] is created to build a client for a specific session.
|
||||
*
|
||||
* @param clientBuilder The [MatrixClientBuilder] to customize.
|
||||
* @param sessionId The [SessionId] for which the [MatrixClientBuilder] is being created.
|
||||
* @return The customized [MatrixClientBuilder].
|
||||
*/
|
||||
suspend fun beforeClientCreationWithSession(clientBuilder: MatrixClientBuilder, sessionId: SessionId): MatrixClientBuilder
|
||||
}
|
||||
|
||||
+6
-1
@@ -18,7 +18,12 @@ import io.element.android.libraries.matrix.api.core.SessionId
|
||||
*/
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DefaultClientBuilderEnterpriseHook : ClientBuilderEnterpriseHook {
|
||||
override suspend fun invoke(clientBuilder: MatrixClientBuilder, sessionId: SessionId): MatrixClientBuilder {
|
||||
override suspend fun beforeClientCreation(clientBuilder: MatrixClientBuilder): MatrixClientBuilder {
|
||||
// No modification
|
||||
return clientBuilder
|
||||
}
|
||||
|
||||
override suspend fun beforeClientCreationWithSession(clientBuilder: MatrixClientBuilder, sessionId: SessionId): MatrixClientBuilder {
|
||||
// No modification
|
||||
return clientBuilder
|
||||
}
|
||||
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright (c) 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.enterprise.test
|
||||
|
||||
import io.element.android.features.enterprise.api.ClientBuilderEnterpriseHook
|
||||
import io.element.android.libraries.matrix.api.MatrixClientBuilder
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
|
||||
class FakeClientBuilderEnterpriseHook(
|
||||
private val beforeClientCreationResult: (MatrixClientBuilder) -> MatrixClientBuilder = { it },
|
||||
private val beforeClientCreationWithSessionResult: (MatrixClientBuilder, SessionId) -> MatrixClientBuilder = { clientBuilder, _ -> clientBuilder },
|
||||
) : ClientBuilderEnterpriseHook {
|
||||
override suspend fun beforeClientCreation(clientBuilder: MatrixClientBuilder): MatrixClientBuilder {
|
||||
return beforeClientCreationResult(clientBuilder)
|
||||
}
|
||||
|
||||
override suspend fun beforeClientCreationWithSession(clientBuilder: MatrixClientBuilder, sessionId: SessionId): MatrixClientBuilder {
|
||||
return beforeClientCreationWithSessionResult(clientBuilder, sessionId)
|
||||
}
|
||||
}
|
||||
+9
-1
@@ -31,6 +31,7 @@ import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.permalink.PermalinkParser
|
||||
import io.element.android.libraries.matrix.api.room.RoomInfo
|
||||
import io.element.android.libraries.matrix.api.roomlist.LatestEventValue
|
||||
import io.element.android.libraries.matrix.api.roomlist.RoomListFilter
|
||||
import io.element.android.libraries.matrix.api.search.MessageSearch
|
||||
import io.element.android.libraries.matrix.api.search.MessageSearchPaginationState
|
||||
import io.element.android.libraries.matrix.api.search.MessageSearchResult
|
||||
@@ -94,7 +95,14 @@ class GlobalSearchPresenter(
|
||||
AsyncData.Uninitialized
|
||||
}
|
||||
|
||||
launch { roomListSearchDataSource.setSearchQuery(queryState.text.toString()) }
|
||||
// Apply the query to the room list search, looking only for joined rooms matching the query
|
||||
launch {
|
||||
roomListSearchDataSource.setSearchQuery(
|
||||
searchQuery = queryState.text.toString(),
|
||||
additionalFilters = RoomListFilter.Joined,
|
||||
)
|
||||
}
|
||||
// Apply the query to the message search too
|
||||
launch {
|
||||
currentMessageSearch.setQuery(queryState.text.toString())
|
||||
.onFailure { Timber.e(it, "Could not set query for message search") }
|
||||
|
||||
+6
-2
@@ -60,11 +60,15 @@ class RoomListSearchDataSource(
|
||||
roomList.updateVisibleRange(visibleRange)
|
||||
}
|
||||
|
||||
suspend fun setSearchQuery(searchQuery: String) = coroutineScope {
|
||||
suspend fun setSearchQuery(searchQuery: String, additionalFilters: RoomListFilter?) = coroutineScope {
|
||||
val filter = if (searchQuery.isBlank()) {
|
||||
RoomListFilter.None
|
||||
} else {
|
||||
RoomListFilter.NormalizedMatchRoomName(searchQuery)
|
||||
if (additionalFilters != null) {
|
||||
RoomListFilter.all(additionalFilters, RoomListFilter.NormalizedMatchRoomName(searchQuery))
|
||||
} else {
|
||||
RoomListFilter.NormalizedMatchRoomName(searchQuery)
|
||||
}
|
||||
}
|
||||
roomList.updateFilter(filter)
|
||||
}
|
||||
|
||||
+1
-1
@@ -39,7 +39,7 @@ class RoomListSearchPresenter(
|
||||
val dataSource = remember { dataSourceFactory.create(coroutineScope) }
|
||||
|
||||
LaunchedEffect(searchQuery.text) {
|
||||
dataSource.setSearchQuery(searchQuery.text.toString())
|
||||
dataSource.setSearchQuery(searchQuery = searchQuery.text.toString(), additionalFilters = null)
|
||||
}
|
||||
|
||||
fun handleEvent(event: RoomListSearchEvent) {
|
||||
|
||||
+6
@@ -23,6 +23,9 @@ import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.permalink.PermalinkParser
|
||||
import io.element.android.libraries.matrix.api.roomlist.RoomListFilter
|
||||
import io.element.android.libraries.matrix.api.roomlist.RoomListFilter.Joined
|
||||
import io.element.android.libraries.matrix.api.roomlist.RoomListFilter.NormalizedMatchRoomName
|
||||
import io.element.android.libraries.matrix.api.roomlist.RoomListService
|
||||
import io.element.android.libraries.matrix.api.search.MessageSearchService
|
||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||
@@ -142,6 +145,9 @@ class GlobalSearchPresenterTest {
|
||||
testScheduler.advanceUntilIdle()
|
||||
roomList.summaries.emit(listOf(aRoomSummary()))
|
||||
|
||||
// The filter is updated to include the search query and only search joined rooms
|
||||
assertThat(roomList.currentFilter.value).isEqualTo(RoomListFilter.all(Joined, NormalizedMatchRoomName("A query")))
|
||||
|
||||
// Wait for the presenter to emit a loading state before the success state
|
||||
consumeItemsUntilPredicate { it.results is AsyncData.Loading }
|
||||
|
||||
|
||||
+8
-1
@@ -51,6 +51,7 @@ import io.element.android.libraries.androidutils.system.toast
|
||||
import io.element.android.libraries.architecture.NodeInputs
|
||||
import io.element.android.libraries.architecture.callback
|
||||
import io.element.android.libraries.architecture.inputs
|
||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||
import io.element.android.libraries.designsystem.utils.OnLifecycleEvent
|
||||
import io.element.android.libraries.di.RoomScope
|
||||
import io.element.android.libraries.di.annotations.ApplicationContext
|
||||
@@ -103,6 +104,7 @@ class MessagesNode(
|
||||
private val roomMemberModerationRenderer: RoomMemberModerationRenderer,
|
||||
private val eventContentValidationCache: EventContentValidationCache,
|
||||
private val emojiPickerRenderer: EmojiPickerRenderer,
|
||||
private val dispatchers: CoroutineDispatchers,
|
||||
) : Node(buildContext, plugins = plugins), MessagesNavigator {
|
||||
data class Inputs(
|
||||
val focusedEventId: EventId?,
|
||||
@@ -111,7 +113,12 @@ class MessagesNode(
|
||||
private val inputs = inputs<Inputs>()
|
||||
private val callback: Callback = callback()
|
||||
|
||||
private val timelineController = TimelineController(room, room.liveTimeline)
|
||||
private val timelineController = TimelineController(
|
||||
room = room,
|
||||
liveTimeline = room.liveTimeline,
|
||||
roomCoroutineScope = room.roomCoroutineScope,
|
||||
dispatchers = dispatchers,
|
||||
)
|
||||
private val presenter = presenterFactory.create(
|
||||
navigator = this,
|
||||
composerPresenter = messageComposerPresenterFactory.create(timelineController, this, threadRoot = null),
|
||||
|
||||
+8
-1
@@ -54,6 +54,7 @@ import io.element.android.libraries.architecture.NodeInputs
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.architecture.callback
|
||||
import io.element.android.libraries.architecture.inputs
|
||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||
import io.element.android.libraries.designsystem.utils.OnLifecycleEvent
|
||||
import io.element.android.libraries.di.RoomScope
|
||||
import io.element.android.libraries.emoji.api.picker.EmojiPickerRenderer
|
||||
@@ -95,6 +96,7 @@ class ThreadedMessagesNode(
|
||||
private val appNavigationStateService: AppNavigationStateService,
|
||||
private val roomMemberModerationRenderer: RoomMemberModerationRenderer,
|
||||
private val emojiPickerRenderer: EmojiPickerRenderer,
|
||||
private val dispatchers: CoroutineDispatchers,
|
||||
) : Node(buildContext, plugins = plugins), MessagesNavigator {
|
||||
data class Inputs(
|
||||
val threadRootEventId: ThreadId,
|
||||
@@ -113,7 +115,12 @@ class ThreadedMessagesNode(
|
||||
*/
|
||||
private suspend fun createPresenter(): Presenter<MessagesState> {
|
||||
val threadedTimeline = room.createTimeline(CreateTimelineParams.Threaded(threadRootEventId = inputs.threadRootEventId)).getOrThrow()
|
||||
val timelineController = TimelineController(room, threadedTimeline)
|
||||
val timelineController = TimelineController(
|
||||
room,
|
||||
liveTimeline = threadedTimeline,
|
||||
roomCoroutineScope = room.roomCoroutineScope,
|
||||
dispatchers = dispatchers,
|
||||
)
|
||||
this.timelineController = timelineController
|
||||
return presenterFactory.create(
|
||||
navigator = this,
|
||||
|
||||
+6
-2
@@ -12,7 +12,10 @@ import dev.zacsweers.metro.ContributesBinding
|
||||
import dev.zacsweers.metro.SingleIn
|
||||
import dev.zacsweers.metro.binding
|
||||
import io.element.android.features.messages.impl.timeline.di.LiveTimeline
|
||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||
import io.element.android.libraries.core.coroutine.childScope
|
||||
import io.element.android.libraries.di.RoomScope
|
||||
import io.element.android.libraries.di.annotations.RoomCoroutineScope
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.ThreadId
|
||||
import io.element.android.libraries.matrix.api.room.CreateTimelineParams
|
||||
@@ -23,7 +26,6 @@ import io.element.android.libraries.matrix.api.timeline.TimelineProvider
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
@@ -47,8 +49,10 @@ import java.util.Optional
|
||||
class TimelineController(
|
||||
private val room: JoinedRoom,
|
||||
@LiveTimeline private val liveTimeline: Timeline,
|
||||
@RoomCoroutineScope private val roomCoroutineScope: CoroutineScope,
|
||||
dispatchers: CoroutineDispatchers,
|
||||
) : Closeable, TimelineProvider {
|
||||
private val coroutineScope = CoroutineScope(SupervisorJob())
|
||||
private val coroutineScope = roomCoroutineScope.childScope(dispatchers.computation, "TimelineController")
|
||||
|
||||
private val liveTimelineFlow = flowOf(liveTimeline)
|
||||
private val detachedTimelineFlow = MutableStateFlow<Optional<Timeline>>(Optional.empty())
|
||||
|
||||
+6
-1
@@ -1484,7 +1484,12 @@ class MessagesPresenterTest {
|
||||
clipboardHelper = clipboardHelper,
|
||||
htmlConverterProvider = FakeHtmlConverterProvider(),
|
||||
buildMeta = aBuildMeta(),
|
||||
timelineController = TimelineController(joinedRoom, timeline),
|
||||
timelineController = TimelineController(
|
||||
room = joinedRoom,
|
||||
liveTimeline = timeline,
|
||||
roomCoroutineScope = backgroundScope,
|
||||
dispatchers = coroutineDispatchers,
|
||||
),
|
||||
permalinkParser = permalinkParser,
|
||||
analyticsService = analyticsService,
|
||||
encryptionService = encryptionService,
|
||||
|
||||
+7
-1
@@ -65,6 +65,7 @@ import io.element.android.tests.testutils.WarmUpRule
|
||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||
import io.element.android.tests.testutils.lambda.value
|
||||
import io.element.android.tests.testutils.test
|
||||
import io.element.android.tests.testutils.testCoroutineDispatchers
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
@@ -307,7 +308,12 @@ class MessageComposerPresenterSlashCommandTest {
|
||||
permissionsPresenterFactory = FakePermissionsPresenterFactory(permissionPresenter),
|
||||
permalinkParser = permalinkParser,
|
||||
permalinkBuilder = permalinkBuilder,
|
||||
timelineController = TimelineController(room, timeline),
|
||||
timelineController = TimelineController(
|
||||
room = room,
|
||||
liveTimeline = timeline,
|
||||
roomCoroutineScope = backgroundScope,
|
||||
dispatchers = testCoroutineDispatchers(),
|
||||
),
|
||||
draftService = draftService,
|
||||
mentionSpanProvider = mentionSpanProvider,
|
||||
pillificationHelper = textPillificationHelper,
|
||||
|
||||
+7
-1
@@ -113,6 +113,7 @@ import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||
import io.element.android.tests.testutils.lambda.value
|
||||
import io.element.android.tests.testutils.robolectric.RobolectricTest
|
||||
import io.element.android.tests.testutils.test
|
||||
import io.element.android.tests.testutils.testCoroutineDispatchers
|
||||
import io.element.android.tests.testutils.waitForPredicate
|
||||
import io.mockk.mockk
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
@@ -1707,7 +1708,12 @@ class MessageComposerPresenterTest : RobolectricTest() {
|
||||
permissionsPresenterFactory = FakePermissionsPresenterFactory(permissionPresenter),
|
||||
permalinkParser = permalinkParser,
|
||||
permalinkBuilder = permalinkBuilder,
|
||||
timelineController = TimelineController(room, timeline),
|
||||
timelineController = TimelineController(
|
||||
room = room,
|
||||
liveTimeline = timeline,
|
||||
roomCoroutineScope = backgroundScope,
|
||||
dispatchers = testCoroutineDispatchers(),
|
||||
),
|
||||
draftService = draftService,
|
||||
mentionSpanProvider = mentionSpanProvider,
|
||||
pillificationHelper = textPillificationHelper,
|
||||
|
||||
+34
-9
@@ -21,8 +21,10 @@ import io.element.android.libraries.matrix.test.timeline.FakeTimeline
|
||||
import io.element.android.libraries.matrix.test.timeline.anEventTimelineItem
|
||||
import io.element.android.tests.testutils.lambda.lambdaError
|
||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||
import io.element.android.tests.testutils.testCoroutineDispatchers
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
||||
@@ -35,7 +37,10 @@ class TimelineControllerTest {
|
||||
liveTimeline = liveTimeline,
|
||||
createTimelineResult = { Result.success(detachedTimeline) }
|
||||
)
|
||||
val sut = TimelineController(room = joinedRoom, liveTimeline = liveTimeline)
|
||||
val sut = createTimelineController(
|
||||
room = joinedRoom,
|
||||
liveTimeline = liveTimeline,
|
||||
)
|
||||
|
||||
sut.activeTimelineFlow().test {
|
||||
awaitItem().also { state ->
|
||||
@@ -74,7 +79,10 @@ class TimelineControllerTest {
|
||||
}
|
||||
}
|
||||
)
|
||||
val sut = TimelineController(joinedRoom, liveTimeline)
|
||||
val sut = createTimelineController(
|
||||
room = joinedRoom,
|
||||
liveTimeline = liveTimeline,
|
||||
)
|
||||
|
||||
sut.activeTimelineFlow().test {
|
||||
awaitItem().also { state ->
|
||||
@@ -102,7 +110,10 @@ class TimelineControllerTest {
|
||||
val joinedRoom = FakeJoinedRoom(
|
||||
liveTimeline = liveTimeline
|
||||
)
|
||||
val sut = TimelineController(room = joinedRoom, liveTimeline = liveTimeline)
|
||||
val sut = createTimelineController(
|
||||
room = joinedRoom,
|
||||
liveTimeline = liveTimeline,
|
||||
)
|
||||
sut.activeTimelineFlow().test {
|
||||
awaitItem().also { state ->
|
||||
assertThat(state).isEqualTo(liveTimeline)
|
||||
@@ -121,7 +132,10 @@ class TimelineControllerTest {
|
||||
liveTimeline = liveTimeline,
|
||||
createTimelineResult = { Result.success(detachedTimeline) }
|
||||
)
|
||||
val sut = TimelineController(room = joinedRoom, liveTimeline = liveTimeline)
|
||||
val sut = createTimelineController(
|
||||
room = joinedRoom,
|
||||
liveTimeline = liveTimeline,
|
||||
)
|
||||
sut.activeTimelineFlow().test {
|
||||
awaitItem().also { state ->
|
||||
assertThat(state).isEqualTo(liveTimeline)
|
||||
@@ -149,7 +163,10 @@ class TimelineControllerTest {
|
||||
val joinedRoom = FakeJoinedRoom(
|
||||
liveTimeline = liveTimeline
|
||||
)
|
||||
val sut = TimelineController(room = joinedRoom, liveTimeline = liveTimeline)
|
||||
val sut = createTimelineController(
|
||||
room = joinedRoom,
|
||||
liveTimeline = liveTimeline,
|
||||
)
|
||||
assertThat(sut.timelineItems().first()).hasSize(1)
|
||||
}
|
||||
|
||||
@@ -171,7 +188,10 @@ class TimelineControllerTest {
|
||||
liveTimeline = liveTimeline,
|
||||
createTimelineResult = { Result.success(detachedTimeline) }
|
||||
)
|
||||
val sut = TimelineController(room = joinedRoom, liveTimeline = liveTimeline)
|
||||
val sut = createTimelineController(
|
||||
room = joinedRoom,
|
||||
liveTimeline = liveTimeline,
|
||||
)
|
||||
sut.activeTimelineFlow().test {
|
||||
sut.focusOnEvent(AN_EVENT_ID, null)
|
||||
awaitItem().also { state ->
|
||||
@@ -196,7 +216,10 @@ class TimelineControllerTest {
|
||||
liveTimeline = liveTimeline,
|
||||
createTimelineResult = { Result.success(detachedTimeline) }
|
||||
)
|
||||
val sut = TimelineController(room = joinedRoom, liveTimeline = liveTimeline)
|
||||
val sut = createTimelineController(
|
||||
room = joinedRoom,
|
||||
liveTimeline = liveTimeline,
|
||||
)
|
||||
|
||||
sut.activeTimelineFlow().test {
|
||||
awaitItem().also { state ->
|
||||
@@ -220,12 +243,14 @@ class TimelineControllerTest {
|
||||
}
|
||||
}
|
||||
|
||||
internal fun createTimelineController(
|
||||
internal fun TestScope.createTimelineController(
|
||||
room: FakeJoinedRoom = FakeJoinedRoom(liveTimeline = FakeTimeline()),
|
||||
liveTimeline: Timeline = FakeTimeline(name = "live"),
|
||||
): TimelineController {
|
||||
return TimelineController(
|
||||
room = room,
|
||||
liveTimeline = liveTimeline
|
||||
liveTimeline = liveTimeline,
|
||||
roomCoroutineScope = backgroundScope,
|
||||
dispatchers = testCoroutineDispatchers(),
|
||||
)
|
||||
}
|
||||
|
||||
+11
-2
@@ -1079,7 +1079,11 @@ class TimelinePresenterTest {
|
||||
val focusedState = consumeItemsUntilPredicate { it.focusRequestState == FocusRequestState.Success(AN_EVENT_ID) }.last()
|
||||
assertThat(focusedState.isLive).isFalse()
|
||||
initialState.eventSink.invoke(TimelineEvent.LoadMore(Timeline.PaginationDirection.FORWARDS))
|
||||
advanceUntilIdle()
|
||||
|
||||
// Give some time for any potential pagination to be called, but it should not be called because the focused event is not rendered yet.
|
||||
// Also, given enough time, a new state should be emitted with isLive = false, because we switched to a focused event timeline.
|
||||
runCurrent()
|
||||
|
||||
assert(paginateLambda).isNeverCalled()
|
||||
assertThat(expectMostRecentItem().isLive).isFalse()
|
||||
}
|
||||
@@ -1906,7 +1910,12 @@ class TimelinePresenterTest {
|
||||
sendPollResponseAction = sendPollResponseAction,
|
||||
sessionPreferencesStore = sessionPreferencesStore,
|
||||
timelineItemIndexer = timelineItemIndexer,
|
||||
timelineController = TimelineController(room, timeline),
|
||||
timelineController = TimelineController(
|
||||
room = room,
|
||||
liveTimeline = timeline,
|
||||
roomCoroutineScope = backgroundScope,
|
||||
dispatchers = testCoroutineDispatchers(),
|
||||
),
|
||||
resolveVerifiedUserSendFailurePresenter = resolveVerifiedUserSendFailurePresenter,
|
||||
typingNotificationPresenter = { aTypingNotificationState() },
|
||||
roomCallStatePresenter = { aStandByCallState() },
|
||||
|
||||
@@ -94,7 +94,6 @@ dependencies {
|
||||
implementation(libs.androidx.datastore.preferences)
|
||||
implementation(libs.serialization.json)
|
||||
api(projects.features.preferences.api)
|
||||
implementation(libs.showkase)
|
||||
|
||||
implementation(platform(libs.network.okhttp.bom))
|
||||
implementation(libs.network.okhttp)
|
||||
|
||||
-10
@@ -8,10 +8,8 @@
|
||||
|
||||
package io.element.android.features.preferences.impl.developer
|
||||
|
||||
import androidx.activity.compose.LocalActivity
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.airbnb.android.showkase.models.Showkase
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.plugin.Plugin
|
||||
@@ -20,7 +18,6 @@ import dev.zacsweers.metro.AssistedInject
|
||||
import io.element.android.annotations.ContributesNode
|
||||
import io.element.android.features.preferences.api.ExtraDeveloperOptionsRenderer
|
||||
import io.element.android.libraries.architecture.callback
|
||||
import io.element.android.libraries.designsystem.showkase.getBrowserIntent
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
|
||||
@ContributesNode(SessionScope::class)
|
||||
@@ -47,17 +44,10 @@ class DeveloperSettingsNode(
|
||||
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
val activity = requireNotNull(LocalActivity.current)
|
||||
fun openShowkase() {
|
||||
val intent = Showkase.getBrowserIntent(activity)
|
||||
activity.startActivity(intent)
|
||||
}
|
||||
|
||||
val state = presenter.present()
|
||||
DeveloperSettingsView(
|
||||
state = state,
|
||||
modifier = modifier,
|
||||
onOpenShowkase = ::openShowkase,
|
||||
onPushHistoryClick = callback::navigateToPushHistory,
|
||||
onBackClick = callback::onDone,
|
||||
extraOptions = { extraDeveloperOptionsRenderer.Render(Modifier) },
|
||||
|
||||
-3
@@ -44,7 +44,6 @@ import io.mhssn.colorpicker.ColorPickerType
|
||||
@Composable
|
||||
fun DeveloperSettingsView(
|
||||
state: DeveloperSettingsState,
|
||||
onOpenShowkase: () -> Unit,
|
||||
onPushHistoryClick: () -> Unit,
|
||||
onBackClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
@@ -84,7 +83,6 @@ fun DeveloperSettingsView(
|
||||
// Note: this is OK to hardcode strings in this debug screen.
|
||||
AppDeveloperSettingsView(
|
||||
state = state.appDeveloperSettingsState,
|
||||
onOpenShowkase = onOpenShowkase,
|
||||
)
|
||||
SessionCategory(deviceId = state.deviceId)
|
||||
NotificationCategory(
|
||||
@@ -255,7 +253,6 @@ internal fun DeveloperSettingsViewPreview(
|
||||
) = ElementPreview {
|
||||
DeveloperSettingsView(
|
||||
state = state,
|
||||
onOpenShowkase = {},
|
||||
onPushHistoryClick = {},
|
||||
onBackClick = {},
|
||||
)
|
||||
|
||||
-10
@@ -7,10 +7,8 @@
|
||||
|
||||
package io.element.android.features.preferences.impl.developer.appsettings
|
||||
|
||||
import androidx.activity.compose.LocalActivity
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.airbnb.android.showkase.models.Showkase
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.plugin.Plugin
|
||||
@@ -21,7 +19,6 @@ import io.element.android.annotations.ContributesNode
|
||||
import io.element.android.features.preferences.api.ExtraDeveloperOptionsRenderer
|
||||
import io.element.android.features.preferences.api.PreferencesEntryPoint
|
||||
import io.element.android.libraries.architecture.callback
|
||||
import io.element.android.libraries.designsystem.showkase.getBrowserIntent
|
||||
|
||||
@ContributesNode(AppScope::class)
|
||||
@AssistedInject
|
||||
@@ -35,17 +32,10 @@ class AppDeveloperSettingsNode(
|
||||
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
val activity = requireNotNull(LocalActivity.current)
|
||||
fun openShowkase() {
|
||||
val intent = Showkase.getBrowserIntent(activity)
|
||||
activity.startActivity(intent)
|
||||
}
|
||||
|
||||
val state = presenter.present()
|
||||
AppDeveloperSettingsPage(
|
||||
state = state,
|
||||
modifier = modifier,
|
||||
onOpenShowkase = ::openShowkase,
|
||||
onBackClick = callback::onDone,
|
||||
extraOptions = { extraDeveloperOptionsRenderer.Render(Modifier) },
|
||||
)
|
||||
|
||||
-3
@@ -21,7 +21,6 @@ import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
@Composable
|
||||
fun AppDeveloperSettingsPage(
|
||||
state: AppDeveloperSettingsState,
|
||||
onOpenShowkase: () -> Unit,
|
||||
onBackClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
extraOptions: @Composable () -> Unit = {},
|
||||
@@ -38,7 +37,6 @@ fun AppDeveloperSettingsPage(
|
||||
) {
|
||||
AppDeveloperSettingsView(
|
||||
state = state,
|
||||
onOpenShowkase = onOpenShowkase,
|
||||
modifier = Modifier.padding(top = 8.dp)
|
||||
)
|
||||
extraOptions()
|
||||
@@ -50,7 +48,6 @@ fun AppDeveloperSettingsPage(
|
||||
internal fun AppDeveloperSettingsPagePreview() = ElementPreview {
|
||||
AppDeveloperSettingsPage(
|
||||
state = anAppDeveloperSettingsState(),
|
||||
onOpenShowkase = {},
|
||||
onBackClick = {},
|
||||
)
|
||||
}
|
||||
|
||||
-10
@@ -42,7 +42,6 @@ import kotlinx.collections.immutable.toImmutableList
|
||||
@Composable
|
||||
fun AppDeveloperSettingsView(
|
||||
state: AppDeveloperSettingsState,
|
||||
onOpenShowkase: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(
|
||||
@@ -82,14 +81,6 @@ fun AppDeveloperSettingsView(
|
||||
)
|
||||
}
|
||||
}
|
||||
PreferenceCategory(title = "Showkase") {
|
||||
ListItem(
|
||||
content = {
|
||||
Text("Open Showkase browser")
|
||||
},
|
||||
onClick = onOpenShowkase
|
||||
)
|
||||
}
|
||||
RageshakePreferencesView(
|
||||
state = state.rageshakeState,
|
||||
)
|
||||
@@ -186,6 +177,5 @@ internal fun AppDeveloperSettingsViewPreview(
|
||||
) = ElementPreview {
|
||||
AppDeveloperSettingsView(
|
||||
state = state,
|
||||
onOpenShowkase = {},
|
||||
)
|
||||
}
|
||||
|
||||
-17
@@ -70,21 +70,6 @@ class DeveloperSettingsViewTest : RobolectricTest() {
|
||||
eventsRecorder.assertSingle(DeveloperSettingsEvent.OpenPushRules)
|
||||
}
|
||||
|
||||
@Config(qualifiers = "h2000dp")
|
||||
@Test
|
||||
fun `clicking on open showkase invokes the expected callback`() = runAndroidComposeUiTest {
|
||||
val eventsRecorder = EventsRecorder<DeveloperSettingsEvent>(expectEvents = false)
|
||||
ensureCalledOnce {
|
||||
setDeveloperSettingsView(
|
||||
state = aDeveloperSettingsState(
|
||||
eventSink = eventsRecorder
|
||||
),
|
||||
onOpenShowkase = it
|
||||
)
|
||||
onNodeWithText("Open Showkase browser").performClick()
|
||||
}
|
||||
}
|
||||
|
||||
@Config(qualifiers = "h2400dp")
|
||||
@Test
|
||||
fun `clicking on clear cache emits the expected event`() = runAndroidComposeUiTest {
|
||||
@@ -101,14 +86,12 @@ class DeveloperSettingsViewTest : RobolectricTest() {
|
||||
|
||||
private fun AndroidComposeUiTest<ComponentActivity>.setDeveloperSettingsView(
|
||||
state: DeveloperSettingsState,
|
||||
onOpenShowkase: () -> Unit = EnsureNeverCalled(),
|
||||
onPushHistoryClick: () -> Unit = EnsureNeverCalled(),
|
||||
onBackClick: () -> Unit = EnsureNeverCalled()
|
||||
) {
|
||||
setContent {
|
||||
DeveloperSettingsView(
|
||||
state = state,
|
||||
onOpenShowkase = onOpenShowkase,
|
||||
onPushHistoryClick = onPushHistoryClick,
|
||||
onBackClick = onBackClick,
|
||||
)
|
||||
|
||||
-17
@@ -64,21 +64,6 @@ class AppDeveloperSettingsPageTest : RobolectricTest() {
|
||||
eventsRecorder.assertSingle(AppDeveloperSettingsEvent.SetCustomElementCallBaseUrl("https://call.element.dev"))
|
||||
}
|
||||
|
||||
@Config(qualifiers = "h2000dp")
|
||||
@Test
|
||||
fun `clicking on open showkase invokes the expected callback`() = runAndroidComposeUiTest {
|
||||
val eventsRecorder = EventsRecorder<AppDeveloperSettingsEvent>(expectEvents = false)
|
||||
ensureCalledOnce {
|
||||
setAppDeveloperSettingsView(
|
||||
state = anAppDeveloperSettingsState(
|
||||
eventSink = eventsRecorder
|
||||
),
|
||||
onOpenShowkase = it
|
||||
)
|
||||
onNodeWithText("Open Showkase browser").performClick()
|
||||
}
|
||||
}
|
||||
|
||||
@Config(qualifiers = "h1024dp")
|
||||
@Test
|
||||
fun `clicking on log level emits the expected event`() = runAndroidComposeUiTest {
|
||||
@@ -96,13 +81,11 @@ class AppDeveloperSettingsPageTest : RobolectricTest() {
|
||||
|
||||
private fun AndroidComposeUiTest<ComponentActivity>.setAppDeveloperSettingsView(
|
||||
state: AppDeveloperSettingsState,
|
||||
onOpenShowkase: () -> Unit = EnsureNeverCalled(),
|
||||
onBackClick: () -> Unit = EnsureNeverCalled(),
|
||||
) {
|
||||
setContent {
|
||||
AppDeveloperSettingsPage(
|
||||
state = state,
|
||||
onOpenShowkase = onOpenShowkase,
|
||||
onBackClick = onBackClick,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -39,9 +39,7 @@ datetime = "0.8.0"
|
||||
serialization_json = "1.11.0"
|
||||
|
||||
#other
|
||||
coil = "3.6.1"
|
||||
# Rollback to 1.0.4, 1.0.5 has this issue: https://github.com/airbnb/Showkase/issues/420
|
||||
showkase = "1.0.5"
|
||||
coil = "3.6.2"
|
||||
# There is some custom logic in `RootFlowNode` that may break because it reuses some Appyx internal APIs.
|
||||
# When upgrading this version, check state restoration still works fine.
|
||||
appyx = "1.7.1"
|
||||
@@ -186,7 +184,7 @@ test_detekt_test = { module = "io.gitlab.arturbosch.detekt:detekt-test", version
|
||||
# https://github.com/matrix-org/matrix-rust-components-kotlin/commits/main/sdk/sdk-android/src/main/kotlin/org/matrix/rustcomponents/sdk/matrix_sdk_ffi.kt
|
||||
# All new features should not be implemented in the pull request that upgrades the version, developers should
|
||||
# only fix API breaks and may add some TODOs.
|
||||
matrix_sdk = "org.matrix.rustcomponents:sdk-android:26.09.08"
|
||||
matrix_sdk = "org.matrix.rustcomponents:sdk-android:26.09.9"
|
||||
|
||||
# Others
|
||||
coil = { module = "io.coil-kt.coil3:coil", version.ref = "coil" }
|
||||
@@ -198,8 +196,6 @@ coil_test = { module = "io.coil-kt.coil3:coil-test", version.ref = "coil" }
|
||||
datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "datetime" }
|
||||
serialization_json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "serialization_json" }
|
||||
kotlinx_collections_immutable = "org.jetbrains.kotlinx:kotlinx-collections-immutable:0.5.2"
|
||||
showkase = { module = "com.airbnb.android:showkase", version.ref = "showkase" }
|
||||
showkase_processor = { module = "com.airbnb.android:showkase-processor", version.ref = "showkase" }
|
||||
jsoup = "org.jsoup:jsoup:1.23.2"
|
||||
appyx_core = { module = "com.bumble.appyx:core", version.ref = "appyx" }
|
||||
molecule-runtime = "app.cash.molecule:molecule-runtime:2.2.0"
|
||||
@@ -230,7 +226,7 @@ color_picker = "io.mhssn:colorpicker:1.0.0"
|
||||
|
||||
# Analytics
|
||||
posthog = "com.posthog:posthog-android:3.60.7"
|
||||
sentry = "io.sentry:sentry-android:8.54.0"
|
||||
sentry = "io.sentry:sentry-android:8.55.0"
|
||||
# main branch can be tested replacing the version with main-SNAPSHOT
|
||||
matrix_analytics_events = "com.github.matrix-org:matrix-analytics-events:0.37.0"
|
||||
|
||||
|
||||
@@ -22,7 +22,6 @@ android {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(libs.showkase)
|
||||
testCommonDependencies(libs)
|
||||
testImplementation(libs.test.roborazzi)
|
||||
testImplementation(libs.test.roborazzi.compose)
|
||||
|
||||
-15
@@ -1,15 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.compound.showkase
|
||||
|
||||
import com.airbnb.android.showkase.annotation.ShowkaseRoot
|
||||
import com.airbnb.android.showkase.annotation.ShowkaseRootModule
|
||||
|
||||
@ShowkaseRoot
|
||||
class CompoundShowkaseRootModule : ShowkaseRootModule
|
||||
+1
-17
@@ -16,19 +16,15 @@ import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.LineHeightStyle
|
||||
import androidx.compose.ui.unit.em
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.airbnb.android.showkase.annotation.ShowkaseTypography
|
||||
import io.element.android.compound.tokens.generated.TypographyTokens
|
||||
|
||||
// 32px (Material) vs 34px, it's the closest one
|
||||
@ShowkaseTypography(name = "M3 Headline Large", group = "Compound")
|
||||
internal val compoundHeadingXlRegular = TypographyTokens.fontHeadingXlRegular
|
||||
|
||||
// both are 28px
|
||||
@ShowkaseTypography(name = "M3 Headline Medium", group = "Compound")
|
||||
internal val compoundHeadingLgRegular = TypographyTokens.fontHeadingLgRegular
|
||||
|
||||
// These are the default M3 values, but we're setting them manually so an update in M3 doesn't break our designs
|
||||
@ShowkaseTypography(name = "M3 Headline Small", group = "Compound")
|
||||
internal val defaultHeadlineSmall = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Normal,
|
||||
@@ -40,39 +36,27 @@ internal val defaultHeadlineSmall = TextStyle(
|
||||
)
|
||||
|
||||
// 22px (Material) vs 20px, it's the closest one
|
||||
@ShowkaseTypography(name = "M3 Title Large", group = "Compound")
|
||||
internal val compoundHeadingMdRegular = TypographyTokens.fontHeadingMdRegular
|
||||
|
||||
// 16px both
|
||||
@ShowkaseTypography(name = "M3 Title Medium", group = "Compound")
|
||||
internal val compoundBodyLgMedium = TypographyTokens.fontBodyLgMedium
|
||||
|
||||
// 14px both
|
||||
@ShowkaseTypography(name = "M3 Title Small", group = "Compound")
|
||||
internal val compoundBodyMdMedium = TypographyTokens.fontBodyMdMedium
|
||||
|
||||
// 16px both
|
||||
@ShowkaseTypography(name = "M3 Body Large", group = "Compound")
|
||||
internal val compoundBodyLgRegular = TypographyTokens.fontBodyLgRegular
|
||||
|
||||
// 14px both
|
||||
@ShowkaseTypography(name = "M3 Body Medium", group = "Compound")
|
||||
internal val compoundBodyMdRegular = TypographyTokens.fontBodyMdRegular
|
||||
|
||||
// 12px both
|
||||
@ShowkaseTypography(name = "M3 Body Small", group = "Compound")
|
||||
internal val compoundBodySmRegular = TypographyTokens.fontBodySmRegular
|
||||
|
||||
// 14px both, Title Small uses the same token so we have to declare it twice
|
||||
@ShowkaseTypography(name = "M3 Label Large", group = "Compound")
|
||||
internal val compoundBodyMdMedium_LabelLarge = TypographyTokens.fontBodyMdMedium
|
||||
|
||||
// 12px both
|
||||
@ShowkaseTypography(name = "M3 Label Medium", group = "Compound")
|
||||
internal val compoundBodySmMedium = TypographyTokens.fontBodySmMedium
|
||||
|
||||
// 11px both
|
||||
@ShowkaseTypography(name = "M3 Label Small", group = "Compound")
|
||||
internal val compoundBodyXsMedium = TypographyTokens.fontBodyXsMedium
|
||||
|
||||
internal val compoundTypography = Typography(
|
||||
@@ -88,7 +72,7 @@ internal val compoundTypography = Typography(
|
||||
bodyLarge = compoundBodyLgRegular,
|
||||
bodyMedium = compoundBodyMdRegular,
|
||||
bodySmall = compoundBodySmRegular,
|
||||
labelLarge = compoundBodyMdMedium_LabelLarge,
|
||||
labelLarge = compoundBodyMdMedium,
|
||||
labelMedium = compoundBodySmMedium,
|
||||
labelSmall = compoundBodyXsMedium,
|
||||
)
|
||||
|
||||
@@ -10,7 +10,6 @@ import extension.testCommonDependencies
|
||||
|
||||
plugins {
|
||||
id("io.element.android-compose-library")
|
||||
alias(libs.plugins.ksp)
|
||||
id("kotlin-parcelize")
|
||||
}
|
||||
|
||||
@@ -20,12 +19,6 @@ android {
|
||||
buildFeatures {
|
||||
buildConfig = true
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
getByName("release") {
|
||||
consumerProguardFiles("consumer-rules.keep")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
@@ -43,8 +36,5 @@ dependencies {
|
||||
implementation(projects.libraries.testtags)
|
||||
implementation(projects.libraries.uiStrings)
|
||||
|
||||
ksp(libs.showkase.processor)
|
||||
implementation(libs.showkase)
|
||||
|
||||
testCommonDependencies(libs)
|
||||
}
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
# Showkase generates this class, and Showkase's ShowkaseBrowserActivity loads it reflectively with
|
||||
# Class.forName(...).getDeclaredConstructor().newInstance(), so its constructor must be kept too.
|
||||
-keep class io.element.android.libraries.designsystem.showkase.DesignSystemShowkaseRootModuleCodegen {
|
||||
<init>();
|
||||
}
|
||||
+2
-2
@@ -14,8 +14,8 @@ import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.unit.Density
|
||||
|
||||
/**
|
||||
* Showkase does not take into account the `fontScale` parameter of the Preview annotation, so alter the
|
||||
* LocalDensity in the CompositionLocalProvider.
|
||||
* Render the content at the given font scale, by altering the `LocalDensity` in the
|
||||
* CompositionLocalProvider. Useful to preview a Composable at several font scales.
|
||||
*/
|
||||
@Composable
|
||||
fun WithFontScale(fontScale: Float, content: @Composable () -> Unit) {
|
||||
|
||||
-15
@@ -1,15 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2022-2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.designsystem.showkase
|
||||
|
||||
import com.airbnb.android.showkase.annotation.ShowkaseRoot
|
||||
import com.airbnb.android.showkase.annotation.ShowkaseRootModule
|
||||
|
||||
@ShowkaseRoot
|
||||
class DesignSystemShowkaseRootModule : ShowkaseRootModule
|
||||
+5
@@ -67,6 +67,11 @@ sealed interface RoomListFilter {
|
||||
*/
|
||||
data object Invite : RoomListFilter
|
||||
|
||||
/**
|
||||
* A filter that matches rooms with Joined membership.
|
||||
*/
|
||||
data object Joined : RoomListFilter
|
||||
|
||||
/**
|
||||
* A filter that matches either Group,People rooms or Space.
|
||||
*/
|
||||
|
||||
+10
-11
@@ -35,7 +35,6 @@ import io.element.android.services.analytics.api.AnalyticsService
|
||||
import io.element.android.services.toolbox.api.systemclock.SystemClock
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.matrix.rustcomponents.sdk.Client
|
||||
import org.matrix.rustcomponents.sdk.ClientBuilder
|
||||
import org.matrix.rustcomponents.sdk.CrossProcessLockConfig
|
||||
import org.matrix.rustcomponents.sdk.RequestConfig
|
||||
@@ -100,7 +99,12 @@ class RustMatrixClientFactory(
|
||||
)
|
||||
.homeserverUrl(sessionData.homeserverUrl)
|
||||
.enableAutomaticBackPagination(featureFlagService.isFeatureEnabled(FeatureFlags.AutomaticBackPagination))
|
||||
.let { (clientBuilderEnterpriseHook(RustMatrixClientBuilder(it), SessionId(sessionData.userId)) as RustMatrixClientBuilder).inner }
|
||||
.let {
|
||||
(clientBuilderEnterpriseHook.beforeClientCreationWithSession(
|
||||
clientBuilder = RustMatrixClientBuilder(it),
|
||||
sessionId = SessionId(sessionData.userId),
|
||||
) as RustMatrixClientBuilder).inner
|
||||
}
|
||||
.use { it.build() }
|
||||
|
||||
client.setMediaRetentionPolicy(
|
||||
@@ -118,14 +122,6 @@ class RustMatrixClientFactory(
|
||||
|
||||
client.restoreSession(sessionData.toSession())
|
||||
|
||||
create(client, sessionData, isMessageSearchAvailable)
|
||||
}
|
||||
|
||||
suspend fun create(
|
||||
client: Client,
|
||||
sessionData: SessionData,
|
||||
isMessageSearchAvailable: Boolean,
|
||||
): RustMatrixClient {
|
||||
val (anonymizedAccessToken, anonymizedRefreshToken) = client.session().anonymizedTokens()
|
||||
|
||||
client.setUtdDelegate(UtdTracker(analyticsService))
|
||||
@@ -135,7 +131,7 @@ class RustMatrixClientFactory(
|
||||
.withOfflineMode()
|
||||
.finish()
|
||||
|
||||
return RustMatrixClient(
|
||||
RustMatrixClient(
|
||||
sessionPaths = sessionData.getSessionPaths(),
|
||||
innerClient = client,
|
||||
sessionStore = sessionStore,
|
||||
@@ -227,6 +223,9 @@ class RustMatrixClientFactory(
|
||||
// Workaround for non-nullable proxy parameter in the SDK, since each call to the ClientBuilder returns a new reference we need to keep
|
||||
proxyProvider.provides()?.let { proxy(it) } ?: this
|
||||
}
|
||||
.let {
|
||||
(clientBuilderEnterpriseHook.beforeClientCreation(RustMatrixClientBuilder(it)) as RustMatrixClientBuilder).inner
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+45
-54
@@ -44,6 +44,7 @@ import io.element.android.libraries.matrix.impl.keys.SecretGenerator
|
||||
import io.element.android.libraries.matrix.impl.mapper.toSessionData
|
||||
import io.element.android.libraries.matrix.impl.paths.SessionPathsFactory
|
||||
import io.element.android.libraries.sessionstorage.api.LoginType
|
||||
import io.element.android.libraries.sessionstorage.api.SessionData
|
||||
import io.element.android.libraries.sessionstorage.api.SessionStore
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.flow.first
|
||||
@@ -101,17 +102,7 @@ class RustMatrixAuthenticationService(
|
||||
runCatchingExceptions {
|
||||
val sessionData = sessionStore.getSession(sessionId.value)
|
||||
if (sessionData != null) {
|
||||
if (sessionData.isTokenValid) {
|
||||
// Use the sessionData.passphrase, which can be null for a previously created session
|
||||
if (sessionData.passphrase == null) {
|
||||
Timber.w("Restoring a session without a passphrase")
|
||||
} else {
|
||||
Timber.w("Restoring a session with a passphrase")
|
||||
}
|
||||
rustMatrixClientFactory.create(sessionData)
|
||||
} else {
|
||||
throw SessionRestorationException.InvalidToken()
|
||||
}
|
||||
restoreSession(sessionData)
|
||||
} else {
|
||||
throw SessionRestorationException.MissingSession(sessionId)
|
||||
}
|
||||
@@ -120,6 +111,19 @@ class RustMatrixAuthenticationService(
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun restoreSession(sessionData: SessionData): MatrixClient {
|
||||
if (!sessionData.isTokenValid) {
|
||||
throw SessionRestorationException.InvalidToken()
|
||||
}
|
||||
// Use the sessionData.passphrase, which can be null for a previously created session
|
||||
if (sessionData.passphrase == null) {
|
||||
Timber.w("Restoring a session without a passphrase")
|
||||
} else {
|
||||
Timber.w("Restoring a session with a passphrase")
|
||||
}
|
||||
return rustMatrixClientFactory.create(sessionData)
|
||||
}
|
||||
|
||||
private fun getDatabaseKey(): ClientSecret {
|
||||
Timber.d("New sessions will be encrypted with a raw key")
|
||||
return secretGenerator.generateKey()
|
||||
@@ -137,7 +141,7 @@ class RustMatrixAuthenticationService(
|
||||
|
||||
client.homeserverLoginDetails().map()
|
||||
}.onFailure {
|
||||
clear(destroyClient = true)
|
||||
clear()
|
||||
}.mapFailure { failure ->
|
||||
Timber.e(failure, "Failed to set homeserver to $homeserver")
|
||||
failure.mapAuthenticationException()
|
||||
@@ -165,18 +169,7 @@ class RustMatrixAuthenticationService(
|
||||
passphrase = pendingKey.formattedAsString(),
|
||||
sessionPaths = currentSessionPaths,
|
||||
)
|
||||
val matrixClient = rustMatrixClientFactory.create(client, sessionData, isMessageSearchAvailable())
|
||||
|
||||
// Apply enterprise hooks to the newly created client as soon as possible
|
||||
clientEnterpriseHook(matrixClient)
|
||||
|
||||
newMatrixClientObservers.forEach { it.invoke(matrixClient) }
|
||||
sessionStore.addSession(sessionData)
|
||||
|
||||
// Clean up the strong reference held here since it's no longer necessary
|
||||
clear(destroyClient = false)
|
||||
|
||||
SessionId(sessionData.userId)
|
||||
finalizeClientCreation(sessionData)
|
||||
}.mapFailure { failure ->
|
||||
Timber.e(failure, "Failed to login")
|
||||
failure.mapAuthenticationException()
|
||||
@@ -305,20 +298,7 @@ class RustMatrixAuthenticationService(
|
||||
passphrase = pendingKey.formattedAsString(),
|
||||
sessionPaths = currentSessionPaths,
|
||||
)
|
||||
val matrixClient = rustMatrixClientFactory.create(client, sessionData, isMessageSearchAvailable())
|
||||
|
||||
// Apply enterprise hooks to the newly created client as soon as possible
|
||||
clientEnterpriseHook(matrixClient)
|
||||
|
||||
matrixClient.waitForKnownVerificationState()
|
||||
|
||||
newMatrixClientObservers.forEach { it.invoke(matrixClient) }
|
||||
sessionStore.addSession(sessionData)
|
||||
|
||||
// Clean up the strong reference held here since it's no longer necessary
|
||||
clear(destroyClient = false)
|
||||
|
||||
SessionId(sessionData.userId)
|
||||
finalizeClientCreation(sessionData)
|
||||
}.mapFailure { failure ->
|
||||
Timber.e(failure, "Failed to login with OAuth")
|
||||
failure.mapAuthenticationException()
|
||||
@@ -357,6 +337,7 @@ class RustMatrixAuthenticationService(
|
||||
sessionPaths = emptySessionPaths,
|
||||
qrCodeData = sdkQrCodeLoginData,
|
||||
)
|
||||
currentClient = client
|
||||
client.newLoginWithQrCodeHandler(
|
||||
oauthConfiguration = oAuthConfiguration,
|
||||
).use {
|
||||
@@ -374,18 +355,7 @@ class RustMatrixAuthenticationService(
|
||||
passphrase = pendingKey.formattedAsString(),
|
||||
sessionPaths = emptySessionPaths,
|
||||
)
|
||||
val matrixClient = rustMatrixClientFactory.create(client, sessionData, isMessageSearchAvailable())
|
||||
|
||||
// Apply enterprise hooks to the newly created client as soon as possible
|
||||
clientEnterpriseHook(matrixClient)
|
||||
|
||||
newMatrixClientObservers.forEach { it.invoke(matrixClient) }
|
||||
sessionStore.addSession(sessionData)
|
||||
|
||||
// Clean up the strong reference held here since it's no longer necessary
|
||||
clear(destroyClient = false)
|
||||
|
||||
SessionId(sessionData.userId)
|
||||
finalizeClientCreation(sessionData)
|
||||
}.mapFailure {
|
||||
when (it) {
|
||||
is QrCodeDecodeException -> QrErrorMapper.map(it)
|
||||
@@ -393,6 +363,8 @@ class RustMatrixAuthenticationService(
|
||||
else -> it
|
||||
}
|
||||
}.onFailure { throwable ->
|
||||
// A QR code login always builds its own client, so it can be disposed of on failure.
|
||||
clear()
|
||||
if (throwable is CancellationException) {
|
||||
throw throwable
|
||||
}
|
||||
@@ -400,6 +372,27 @@ class RustMatrixAuthenticationService(
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun finalizeClientCreation(sessionData: SessionData): SessionId {
|
||||
// Close the client which was used to perform the login before creating the final client.
|
||||
// Both use the same session paths, so their SQLite stores must never be opened at the same time.
|
||||
clear()
|
||||
|
||||
val matrixClient = restoreSession(sessionData)
|
||||
// Apply enterprise hooks to the newly created client as soon as possible
|
||||
clientEnterpriseHook(matrixClient)
|
||||
|
||||
matrixClient.waitForKnownVerificationState()
|
||||
|
||||
newMatrixClientObservers.forEach { it.invoke(matrixClient) }
|
||||
sessionStore.addSession(sessionData)
|
||||
|
||||
// The session paths now hold the data of the account which has just been logged in, so forget
|
||||
// them: they must not be deleted by the rotateSessionPath() of the next login attempt.
|
||||
sessionPaths = null
|
||||
|
||||
return SessionId(sessionData.userId)
|
||||
}
|
||||
|
||||
private suspend fun makeClient(
|
||||
sessionPaths: SessionPaths,
|
||||
config: suspend ClientBuilder.() -> ClientBuilder,
|
||||
@@ -449,10 +442,8 @@ class RustMatrixAuthenticationService(
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun clear(destroyClient: Boolean) {
|
||||
if (destroyClient) {
|
||||
currentClient?.close()
|
||||
}
|
||||
private fun clear() {
|
||||
currentClient?.close()
|
||||
currentClient = null
|
||||
}
|
||||
|
||||
|
||||
+2
@@ -16,6 +16,7 @@ import org.matrix.rustcomponents.sdk.RoomListEntriesDynamicFilterKind.Deduplicat
|
||||
import org.matrix.rustcomponents.sdk.RoomListEntriesDynamicFilterKind.Favourite
|
||||
import org.matrix.rustcomponents.sdk.RoomListEntriesDynamicFilterKind.Identifiers
|
||||
import org.matrix.rustcomponents.sdk.RoomListEntriesDynamicFilterKind.Invite
|
||||
import org.matrix.rustcomponents.sdk.RoomListEntriesDynamicFilterKind.Joined
|
||||
import org.matrix.rustcomponents.sdk.RoomListEntriesDynamicFilterKind.NonLeft
|
||||
import org.matrix.rustcomponents.sdk.RoomListEntriesDynamicFilterKind.NonSpace
|
||||
import org.matrix.rustcomponents.sdk.RoomListEntriesDynamicFilterKind.None
|
||||
@@ -73,6 +74,7 @@ internal object RoomListFilterMapper {
|
||||
is RoomListFilter.NormalizedMatchRoomName -> NormalizedMatchRoomName(
|
||||
pattern = filter.pattern
|
||||
)
|
||||
RoomListFilter.Joined -> Joined
|
||||
RoomListFilter.Invite -> Invite
|
||||
}
|
||||
}
|
||||
|
||||
+5
-2
@@ -9,11 +9,13 @@
|
||||
package io.element.android.libraries.matrix.impl
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.enterprise.test.FakeClientBuilderEnterpriseHook
|
||||
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.matrix.impl.auth.FakeProxyProvider
|
||||
import io.element.android.libraries.matrix.impl.room.FakeTimelineEventFilterFactory
|
||||
import io.element.android.libraries.matrix.impl.storage.FakeSqliteStoreBuilderProvider
|
||||
import io.element.android.libraries.matrix.impl.storage.SqliteStoreBuilderProvider
|
||||
import io.element.android.libraries.network.useragent.SimpleUserAgentProvider
|
||||
import io.element.android.libraries.sessionstorage.api.SessionStore
|
||||
import io.element.android.libraries.sessionstorage.test.InMemorySessionStore
|
||||
@@ -51,6 +53,7 @@ fun TestScope.createRustMatrixClientFactory(
|
||||
),
|
||||
clientBuilderProvider: ClientBuilderProvider = FakeClientBuilderProvider(),
|
||||
workManagerScheduler: FakeWorkManagerScheduler = FakeWorkManagerScheduler(),
|
||||
sqliteStoreBuilderProvider: SqliteStoreBuilderProvider = FakeSqliteStoreBuilderProvider(),
|
||||
) = RustMatrixClientFactory(
|
||||
cacheDirectory = cacheDirectory,
|
||||
appCoroutineScope = backgroundScope,
|
||||
@@ -63,7 +66,7 @@ fun TestScope.createRustMatrixClientFactory(
|
||||
featureFlagService = FakeFeatureFlagService(),
|
||||
timelineEventFilterFactory = FakeTimelineEventFilterFactory(),
|
||||
clientBuilderProvider = clientBuilderProvider,
|
||||
sqliteStoreBuilderProvider = FakeSqliteStoreBuilderProvider(),
|
||||
sqliteStoreBuilderProvider = sqliteStoreBuilderProvider,
|
||||
workManagerScheduler = workManagerScheduler,
|
||||
clientBuilderEnterpriseHook = { builder, _ -> builder },
|
||||
clientBuilderEnterpriseHook = FakeClientBuilderEnterpriseHook(),
|
||||
)
|
||||
|
||||
+2
-1
@@ -8,6 +8,7 @@
|
||||
package io.element.android.libraries.matrix.impl
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.enterprise.test.FakeClientBuilderEnterpriseHook
|
||||
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
||||
import io.element.android.libraries.matrix.impl.auth.FakeProxyProvider
|
||||
import io.element.android.libraries.matrix.impl.paths.SessionPathsFactory
|
||||
@@ -77,6 +78,6 @@ class RustTemporaryMatrixClientFactoryTest {
|
||||
clientBuilderProvider = clientBuilderProvider,
|
||||
sqliteStoreBuilderProvider = FakeSqliteStoreBuilderProvider(),
|
||||
workManagerScheduler = workManagerScheduler,
|
||||
clientBuilderEnterpriseHook = { builder, _ -> builder },
|
||||
clientBuilderEnterpriseHook = FakeClientBuilderEnterpriseHook(),
|
||||
)
|
||||
}
|
||||
|
||||
+187
-5
@@ -12,25 +12,44 @@ import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.enterprise.api.EnterpriseService
|
||||
import io.element.android.features.enterprise.test.FakeEnterpriseService
|
||||
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
||||
import io.element.android.libraries.matrix.api.paths.SessionPaths
|
||||
import io.element.android.libraries.matrix.impl.ClientBuilderProvider
|
||||
import io.element.android.libraries.matrix.impl.FakeClientBuilderProvider
|
||||
import io.element.android.libraries.matrix.impl.auth.qrlogin.SdkQrCodeLoginData
|
||||
import io.element.android.libraries.matrix.impl.createRustMatrixClientFactory
|
||||
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiClient
|
||||
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiClientBuilder
|
||||
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiHomeserverLoginDetails
|
||||
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiLoginWithQrCodeHandler
|
||||
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiQrCodeData
|
||||
import io.element.android.libraries.matrix.impl.paths.SessionPathsFactory
|
||||
import io.element.android.libraries.matrix.impl.storage.FakeSqliteStoreBuilder
|
||||
import io.element.android.libraries.matrix.impl.storage.FakeSqliteStoreBuilderProvider
|
||||
import io.element.android.libraries.matrix.impl.storage.SqliteStoreBuilder
|
||||
import io.element.android.libraries.matrix.impl.storage.SqliteStoreBuilderProvider
|
||||
import io.element.android.libraries.matrix.test.A_HOMESERVER_URL
|
||||
import io.element.android.libraries.matrix.test.A_USER_ID
|
||||
import io.element.android.libraries.matrix.test.auth.FakeOAuthRedirectUrlProvider
|
||||
import io.element.android.libraries.matrix.test.core.aBuildMeta
|
||||
import io.element.android.libraries.sessionstorage.api.SessionStore
|
||||
import io.element.android.libraries.sessionstorage.test.InMemorySessionStore
|
||||
import io.element.android.libraries.workmanager.test.FakeWorkManagerScheduler
|
||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||
import io.element.android.tests.testutils.testCoroutineDispatchers
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.rules.TemporaryFolder
|
||||
import org.matrix.rustcomponents.sdk.Client
|
||||
import org.matrix.rustcomponents.sdk.ClientBuilder
|
||||
import org.matrix.rustcomponents.sdk.HumanQrLoginException
|
||||
import java.io.File
|
||||
|
||||
class RustMatrixAuthenticationServiceTest {
|
||||
@get:Rule
|
||||
val temporaryFolder = TemporaryFolder()
|
||||
|
||||
@Test
|
||||
fun `setHomeserver is successful`() = runTest {
|
||||
val sut = createRustMatrixAuthenticationService(
|
||||
@@ -74,20 +93,183 @@ class RustMatrixAuthenticationServiceTest {
|
||||
closeResult.assertions().isCalledOnce()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `login closes the client used to log in before building the client of the session`() = runTest {
|
||||
val events = mutableListOf<String>()
|
||||
val sut = createRustMatrixAuthenticationService(
|
||||
clientBuilderProvider = FakeSequentialClientBuilderProvider(
|
||||
{
|
||||
events.add("build login client")
|
||||
FakeFfiClient(
|
||||
homeserverLoginDetailsResult = { FakeFfiHomeserverLoginDetails() },
|
||||
loginResult = { _, _ -> },
|
||||
closeResult = { events.add("close login client") },
|
||||
)
|
||||
},
|
||||
{
|
||||
events.add("build session client")
|
||||
FakeFfiClient(withUtdHook = {})
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
assertThat(sut.setHomeserver("matrix.org").isSuccess).isTrue()
|
||||
assertThat(sut.login("alice", "password").getOrNull()).isEqualTo(A_USER_ID)
|
||||
|
||||
// The two clients share the same session paths, so the login one must be closed first.
|
||||
assertThat(events).containsExactly("build login client", "close login client", "build session client").inOrder()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `loginWithQrCode closes the client used to log in before building the client of the session`() = runTest {
|
||||
val events = mutableListOf<String>()
|
||||
val sut = createRustMatrixAuthenticationService(
|
||||
clientBuilderProvider = FakeSequentialClientBuilderProvider(
|
||||
{
|
||||
events.add("build login client")
|
||||
FakeFfiClient(
|
||||
newLoginWithQrCodeHandlerResult = { FakeFfiLoginWithQrCodeHandler() },
|
||||
closeResult = { events.add("close login client") },
|
||||
)
|
||||
},
|
||||
{
|
||||
events.add("build session client")
|
||||
FakeFfiClient(withUtdHook = {})
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
val result = sut.loginWithQrCode(aSdkQrCodeLoginData()) {}
|
||||
|
||||
assertThat(result.getOrNull()).isEqualTo(A_USER_ID)
|
||||
assertThat(events).containsExactly("build login client", "close login client", "build session client").inOrder()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `loginWithQrCode closes the client it created when the login fails`() = runTest {
|
||||
val closeResult = lambdaRecorder<Unit> {}
|
||||
val sut = createRustMatrixAuthenticationService(
|
||||
clientBuilderProvider = FakeClientBuilderProvider(
|
||||
provideResult = {
|
||||
FakeFfiClientBuilder(
|
||||
buildResult = {
|
||||
FakeFfiClient(
|
||||
newLoginWithQrCodeHandlerResult = {
|
||||
FakeFfiLoginWithQrCodeHandler(
|
||||
scanResult = { throw HumanQrLoginException.Unknown() },
|
||||
)
|
||||
},
|
||||
closeResult = closeResult,
|
||||
)
|
||||
},
|
||||
)
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
assertThat(sut.loginWithQrCode(aSdkQrCodeLoginData()) {}.isFailure).isTrue()
|
||||
closeResult.assertions().isCalledOnce()
|
||||
}
|
||||
|
||||
private fun aSdkQrCodeLoginData() = SdkQrCodeLoginData(
|
||||
FakeFfiQrCodeData(
|
||||
baseUrlResult = { A_HOMESERVER_URL },
|
||||
)
|
||||
)
|
||||
|
||||
/**
|
||||
* A [ClientBuilderProvider] handing out one [Client] per call, in order.
|
||||
*/
|
||||
private class FakeSequentialClientBuilderProvider(
|
||||
private vararg val clients: () -> Client,
|
||||
) : ClientBuilderProvider {
|
||||
private var index = 0
|
||||
override fun provide(): ClientBuilder = FakeFfiClientBuilder(buildResult = clients[index++])
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a new login does not delete the session data of the account which has just been logged in`() = runTest {
|
||||
val storeBuilderProvider = SessionDirectoryCreatingSqliteStoreBuilderProvider()
|
||||
val sut = createRustMatrixAuthenticationService(
|
||||
clientBuilderProvider = FakeSequentialClientBuilderProvider(
|
||||
{ aLoginFakeFfiClient() },
|
||||
{ FakeFfiClient(withUtdHook = {}) },
|
||||
{ aLoginFakeFfiClient() },
|
||||
),
|
||||
sessionPathsFactory = SessionPathsFactory(temporaryFolder.newFolder("base"), temporaryFolder.newFolder("cache")),
|
||||
sqliteStoreBuilderProvider = storeBuilderProvider,
|
||||
)
|
||||
|
||||
assertThat(sut.setHomeserver("matrix.org").isSuccess).isTrue()
|
||||
assertThat(sut.login("alice", "password").getOrNull()).isEqualTo(A_USER_ID)
|
||||
val loggedInSessionPaths = storeBuilderProvider.providedSessionPaths.first()
|
||||
|
||||
// Adding another account rotates the session paths, which must not touch the previous account.
|
||||
assertThat(sut.setHomeserver("matrix.org").isSuccess).isTrue()
|
||||
|
||||
assertThat(loggedInSessionPaths.fileDirectory.exists()).isTrue()
|
||||
assertThat(loggedInSessionPaths.cacheDirectory.exists()).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a new login deletes the session data of a previous failed login attempt`() = runTest {
|
||||
val storeBuilderProvider = SessionDirectoryCreatingSqliteStoreBuilderProvider()
|
||||
val sut = createRustMatrixAuthenticationService(
|
||||
clientBuilderProvider = FakeSequentialClientBuilderProvider(
|
||||
{ aLoginFakeFfiClient() },
|
||||
{ throw IllegalStateException("Failed to build the client of the session") },
|
||||
{ aLoginFakeFfiClient() },
|
||||
),
|
||||
sessionPathsFactory = SessionPathsFactory(temporaryFolder.newFolder("base"), temporaryFolder.newFolder("cache")),
|
||||
sqliteStoreBuilderProvider = storeBuilderProvider,
|
||||
)
|
||||
|
||||
assertThat(sut.setHomeserver("matrix.org").isSuccess).isTrue()
|
||||
assertThat(sut.login("alice", "password").isFailure).isTrue()
|
||||
val abandonedSessionPaths = storeBuilderProvider.providedSessionPaths.first()
|
||||
|
||||
assertThat(sut.setHomeserver("matrix.org").isSuccess).isTrue()
|
||||
|
||||
assertThat(abandonedSessionPaths.fileDirectory.exists()).isFalse()
|
||||
assertThat(abandonedSessionPaths.cacheDirectory.exists()).isFalse()
|
||||
}
|
||||
|
||||
private fun aLoginFakeFfiClient() = FakeFfiClient(
|
||||
homeserverLoginDetailsResult = { FakeFfiHomeserverLoginDetails() },
|
||||
loginResult = { _, _ -> },
|
||||
)
|
||||
|
||||
/**
|
||||
* A [SqliteStoreBuilderProvider] which creates the session directories, like the SDK does when it
|
||||
* opens its stores, and records them so that a test can assert on their lifecycle.
|
||||
*/
|
||||
private class SessionDirectoryCreatingSqliteStoreBuilderProvider : SqliteStoreBuilderProvider {
|
||||
val providedSessionPaths = mutableListOf<SessionPaths>()
|
||||
|
||||
override fun provide(sessionPaths: SessionPaths): SqliteStoreBuilder {
|
||||
sessionPaths.fileDirectory.mkdirs()
|
||||
sessionPaths.cacheDirectory.mkdirs()
|
||||
providedSessionPaths.add(sessionPaths)
|
||||
return FakeSqliteStoreBuilder()
|
||||
}
|
||||
}
|
||||
|
||||
private fun TestScope.createRustMatrixAuthenticationService(
|
||||
sessionStore: SessionStore = InMemorySessionStore(),
|
||||
sessionStore: SessionStore = InMemorySessionStore(updateUserProfileResult = { _, _, _ -> }),
|
||||
clientBuilderProvider: ClientBuilderProvider = FakeClientBuilderProvider(),
|
||||
enterpriseService: EnterpriseService = FakeEnterpriseService(),
|
||||
sessionPathsFactory: SessionPathsFactory = SessionPathsFactory(File("/base"), File("/cache")),
|
||||
sqliteStoreBuilderProvider: SqliteStoreBuilderProvider = FakeSqliteStoreBuilderProvider(),
|
||||
): RustMatrixAuthenticationService {
|
||||
val baseDirectory = File("/base")
|
||||
val cacheDirectory = File("/cache")
|
||||
val rustMatrixClientFactory = createRustMatrixClientFactory(
|
||||
cacheDirectory = cacheDirectory,
|
||||
cacheDirectory = File("/cache"),
|
||||
sessionStore = sessionStore,
|
||||
clientBuilderProvider = clientBuilderProvider,
|
||||
workManagerScheduler = FakeWorkManagerScheduler(submitLambda = {}),
|
||||
sqliteStoreBuilderProvider = sqliteStoreBuilderProvider,
|
||||
)
|
||||
return RustMatrixAuthenticationService(
|
||||
sessionPathsFactory = SessionPathsFactory(baseDirectory, cacheDirectory),
|
||||
sessionPathsFactory = sessionPathsFactory,
|
||||
coroutineDispatchers = testCoroutineDispatchers(),
|
||||
sessionStore = sessionStore,
|
||||
rustMatrixClientFactory = rustMatrixClientFactory,
|
||||
|
||||
+12
@@ -25,10 +25,12 @@ import org.matrix.rustcomponents.sdk.Encryption
|
||||
import org.matrix.rustcomponents.sdk.HomeserverCapabilities
|
||||
import org.matrix.rustcomponents.sdk.HomeserverLoginDetails
|
||||
import org.matrix.rustcomponents.sdk.IgnoredUsersListener
|
||||
import org.matrix.rustcomponents.sdk.LoginWithQrCodeHandler
|
||||
import org.matrix.rustcomponents.sdk.NoHandle
|
||||
import org.matrix.rustcomponents.sdk.NotificationClient
|
||||
import org.matrix.rustcomponents.sdk.NotificationProcessSetup
|
||||
import org.matrix.rustcomponents.sdk.NotificationSettings
|
||||
import org.matrix.rustcomponents.sdk.OAuthConfiguration
|
||||
import org.matrix.rustcomponents.sdk.ProfileListener
|
||||
import org.matrix.rustcomponents.sdk.PusherIdentifiers
|
||||
import org.matrix.rustcomponents.sdk.PusherKind
|
||||
@@ -57,6 +59,8 @@ class FakeFfiClient(
|
||||
private val withUtdHook: (UnableToDecryptDelegate) -> Unit = { lambdaError() },
|
||||
private val getProfileResult: (String) -> UserProfile = { aRustUserProfile() },
|
||||
private val homeserverLoginDetailsResult: () -> HomeserverLoginDetails = { lambdaError() },
|
||||
private val loginResult: (String, String) -> Unit = { _, _ -> lambdaError() },
|
||||
private val newLoginWithQrCodeHandlerResult: () -> LoginWithQrCodeHandler = { lambdaError() },
|
||||
private val getStoreSizesResult: () -> StoreSizes = { lambdaError() },
|
||||
private val createRoomResult: (CreateRoomParameters) -> String = { lambdaError() },
|
||||
private val homeserverCapabilities: HomeserverCapabilities = FakeFfiHomeserverCapabilities(),
|
||||
@@ -122,6 +126,14 @@ class FakeFfiClient(
|
||||
return homeserverLoginDetailsResult()
|
||||
}
|
||||
|
||||
override suspend fun login(username: String, password: String, initialDeviceName: String?, deviceId: String?) {
|
||||
loginResult(username, password)
|
||||
}
|
||||
|
||||
override fun newLoginWithQrCodeHandler(oauthConfiguration: OAuthConfiguration): LoginWithQrCodeHandler {
|
||||
return newLoginWithQrCodeHandlerResult()
|
||||
}
|
||||
|
||||
override suspend fun setMediaRetentionPolicy(policy: MediaRetentionPolicy) {}
|
||||
|
||||
override suspend fun getStoreSizes(): StoreSizes {
|
||||
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* Copyright (c) 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.matrix.impl.fixtures.fakes
|
||||
|
||||
import org.matrix.rustcomponents.sdk.GeneratedQrLoginProgressListener
|
||||
import org.matrix.rustcomponents.sdk.LoginWithQrCodeHandler
|
||||
import org.matrix.rustcomponents.sdk.NoHandle
|
||||
import org.matrix.rustcomponents.sdk.QrCodeData
|
||||
import org.matrix.rustcomponents.sdk.QrLoginProgress
|
||||
import org.matrix.rustcomponents.sdk.QrLoginProgressListener
|
||||
|
||||
class FakeFfiLoginWithQrCodeHandler(
|
||||
private val generateResult: suspend () -> Unit = {},
|
||||
private val scanResult: suspend (QrCodeData) -> Unit = {},
|
||||
) : LoginWithQrCodeHandler(NoHandle) {
|
||||
private var scanProgressListener: QrLoginProgressListener? = null
|
||||
|
||||
override suspend fun generate(progressListener: GeneratedQrLoginProgressListener) {
|
||||
generateResult()
|
||||
}
|
||||
|
||||
override suspend fun scan(qrCodeData: QrCodeData, progressListener: QrLoginProgressListener) {
|
||||
scanProgressListener = progressListener
|
||||
scanResult(qrCodeData)
|
||||
}
|
||||
|
||||
fun emitScanProgress(progress: QrLoginProgress) {
|
||||
scanProgressListener?.onUpdate(progress)
|
||||
}
|
||||
|
||||
override fun close() = Unit
|
||||
}
|
||||
+5
@@ -14,12 +14,17 @@ import org.matrix.rustcomponents.sdk.QrCodeData
|
||||
|
||||
class FakeFfiQrCodeData(
|
||||
private val serverNameResult: () -> String? = { lambdaError() },
|
||||
private val baseUrlResult: () -> String? = { lambdaError() },
|
||||
private val toBytesResult: () -> ByteArray = { lambdaError() },
|
||||
) : QrCodeData(NoHandle) {
|
||||
override fun serverName(): String? {
|
||||
return serverNameResult()
|
||||
}
|
||||
|
||||
override fun baseUrl(): String? {
|
||||
return baseUrlResult()
|
||||
}
|
||||
|
||||
override fun toBytes(): ByteArray {
|
||||
return toBytesResult()
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ private const val versionMonth = 9
|
||||
* Release number in the month. Value must be in [0,99].
|
||||
* Do not update this value. it is updated by the release script.
|
||||
*/
|
||||
private const val versionReleaseNumber = 1
|
||||
private const val versionReleaseNumber = 2
|
||||
|
||||
object Versions {
|
||||
/**
|
||||
|
||||
@@ -107,13 +107,8 @@ fun Project.setupKover() {
|
||||
// Exclude generated classes.
|
||||
"*_Module",
|
||||
"*_AssistedFactory",
|
||||
"com.airbnb.android.showkase*",
|
||||
"io.element.android.libraries.designsystem.showkase.*",
|
||||
"*ComposableSingletons$*",
|
||||
"*BuildConfig",
|
||||
// Generated by Showkase
|
||||
"*Ioelementandroid*PreviewKt$*",
|
||||
"*Ioelementandroid*PreviewKt",
|
||||
// Other
|
||||
// We do not cover Nodes (normally covered by maestro, but code coverage is not computed with maestro)
|
||||
"*Node",
|
||||
|
||||
+13
-1
@@ -25,7 +25,19 @@ dependencyResolutionManagement {
|
||||
includeModule("com.github.philburk", "jsyn")
|
||||
}
|
||||
}
|
||||
google()
|
||||
// Check for official Android-related packages only in Google's maven repo
|
||||
exclusiveContent {
|
||||
forRepository {
|
||||
google()
|
||||
}
|
||||
filter {
|
||||
includeGroupByRegex("android\\..*")
|
||||
includeGroupByRegex("androidx\\..*")
|
||||
includeGroupByRegex("com\\.android\\.tools.*")
|
||||
includeGroupByRegex("com\\.google\\.firebase.*")
|
||||
includeGroupByRegex("com\\.google\\.android.*")
|
||||
}
|
||||
}
|
||||
mavenCentral()
|
||||
maven {
|
||||
url = uri("https://repo1.maven.org/maven2/")
|
||||
|
||||
@@ -105,6 +105,7 @@ class KonsistClassNameTest {
|
||||
"FakeListenableFuture",
|
||||
"FakeMediaStoreContentProvider",
|
||||
"FakeParentNode",
|
||||
"FakeSequentialClientBuilderProvider",
|
||||
)
|
||||
.assertTrue {
|
||||
val interfaceName = it.name
|
||||
|
||||
@@ -40,8 +40,6 @@ dependencies {
|
||||
)
|
||||
}
|
||||
|
||||
implementation(libs.showkase)
|
||||
|
||||
// TODO There is a Resources.NotFoundException maybe due to the mipmap, even if we have
|
||||
// `testOptions { unitTests.isIncludeAndroidResources = true }` in the app build.gradle.kts file
|
||||
// implementation(projects.app)
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { schedule } from 'danger'
|
||||
|
||||
/**
|
||||
* Ref and documentation: https://github.com/damian-burke/danger-plugin-lint-report
|
||||
* This file will check all the error in XML Checkstyle format.
|
||||
* It covers, lint, ktlint, and detekt errors
|
||||
*/
|
||||
|
||||
const reporter = require("danger-plugin-lint-report")
|
||||
schedule(reporter.scan({
|
||||
/**
|
||||
* File mask used to find XML checkstyle reports.
|
||||
*/
|
||||
fileMask: "**/reports/**/**.xml",
|
||||
/**
|
||||
* If set to true, the severity will be used to switch between the different message formats (message, warn, fail).
|
||||
*/
|
||||
reportSeverity: true,
|
||||
/**
|
||||
* If set to true, only issues will be reported that are contained in the current changeset (line comparison).
|
||||
* If set to false, all issues that are in modified files will be reported.
|
||||
*/
|
||||
requireLineModification: false,
|
||||
/**
|
||||
* Optional: Sets a prefix foreach violation message.
|
||||
* This can be useful if there are multiple reports being parsed to make them distinguishable.
|
||||
*/
|
||||
// outputPrefix?: ""
|
||||
|
||||
/**
|
||||
* Optional: If set to true, it will remove duplicate violations.
|
||||
*/
|
||||
removeDuplicates: true,
|
||||
}))
|
||||
@@ -1,121 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
const {danger, warn} = require('danger')
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
|
||||
/**
|
||||
* Note: if you update the checks in this file, please also update the file ./docs/danger.md
|
||||
*/
|
||||
|
||||
// Useful to see what we got in danger object
|
||||
// warn(JSON.stringify(danger))
|
||||
|
||||
const pr = danger.github.pr
|
||||
const github = danger.github
|
||||
// User who has created the PR.
|
||||
const user = pr.user.login
|
||||
const modified = danger.git.modified_files
|
||||
const created = danger.git.created_files
|
||||
const editedFiles = [...modified, ...created]
|
||||
|
||||
// Check that the PR has a description
|
||||
if (pr.body.length == 0) {
|
||||
warn("Please provide a description for this PR.")
|
||||
}
|
||||
|
||||
// Warn when there is a big PR
|
||||
if (editedFiles.length > 50) {
|
||||
message("This pull request seems relatively large. Please consider splitting it into multiple smaller ones.")
|
||||
}
|
||||
|
||||
// Request a correct title for each PR
|
||||
if (pr.title.endsWith("…")) {
|
||||
fail("Please provide a complete title that can be used as a changelog entry.")
|
||||
}
|
||||
|
||||
// Request a `PR-` label for each PR
|
||||
if (pr.labels.filter((label) => label.name.startsWith("PR-")).length != 1) {
|
||||
fail("Please add a `PR-` label to categorise the changelog entry.")
|
||||
}
|
||||
|
||||
// check that frozen classes have not been modified
|
||||
const frozenClasses = [
|
||||
]
|
||||
|
||||
frozenClasses.forEach(frozen => {
|
||||
if (editedFiles.some(file => file.endsWith(frozen))) {
|
||||
fail("Frozen class `" + frozen + "` has been modified. Please do not modify frozen class.")
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const previewAnnotations = [
|
||||
'androidx.compose.ui.tooling.preview.Preview',
|
||||
'io.element.android.libraries.designsystem.preview.PreviewWithLargeHeight',
|
||||
'io.element.android.libraries.designsystem.preview.PreviewsDayNight'
|
||||
]
|
||||
|
||||
const filesWithPreviews = editedFiles.filter(file => file.endsWith(".kt")).filter(file => {
|
||||
const content = fs.readFileSync(file);
|
||||
return previewAnnotations.some((ann) => content.includes("import " + ann));
|
||||
})
|
||||
|
||||
const composablePreviewProviderContents = fs.readFileSync('tests/uitests/src/test/kotlin/base/ComposablePreviewProvider.kt');
|
||||
const packageTreesRegex = /private val PACKAGE_TREES = arrayOf\(([\w\W]+?)\n\)/gm;
|
||||
const packageTreesMatch = packageTreesRegex.exec(composablePreviewProviderContents)[1];
|
||||
const scannedPreviewPackageTrees = packageTreesMatch
|
||||
.replaceAll("\"", "")
|
||||
.replaceAll(",", "")
|
||||
.split('\n').map((line) => line.trim())
|
||||
.filter((line) => line.length > 0);
|
||||
|
||||
const previewPackagesNotIncludedInScreenshotTests = filesWithPreviews.map((file) => {
|
||||
const content = fs.readFileSync(file);
|
||||
const packageRegex = /package\s+([a-zA-Z0-9.]+)/;
|
||||
const packageMatch = packageRegex.exec(content);
|
||||
|
||||
if (!packageMatch || packageMatch.length != 2) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return packageMatch[1];
|
||||
|
||||
|
||||
}).filter((package) => {
|
||||
if (!package) {
|
||||
return false;
|
||||
}
|
||||
if (!scannedPreviewPackageTrees.some((prefix) => package.includes(prefix))) {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
if (previewPackagesNotIncludedInScreenshotTests.length > 0) {
|
||||
const packagesList = previewPackagesNotIncludedInScreenshotTests.map((p) => '- `' + p + '`').join("\n");
|
||||
warn("You have made changes to a file containing a `@Preview` annotated function but its package name prefix is not included in the `ComposablePreviewProvider`.\nPackages missing in `tests/uitests/src/test/kotlin/base/ComposablePreviewProvider.kt`: \n" + packagesList);
|
||||
}
|
||||
|
||||
// Check for pngs on resources
|
||||
const hasPngs = editedFiles.filter(file => {
|
||||
file.toLowerCase().endsWith(".png") && !file.includes("snapshots/images/") // Exclude screenshots
|
||||
}).length > 0
|
||||
if (hasPngs) {
|
||||
warn("You seem to have made changes to some images. Please consider using an vector drawable.")
|
||||
}
|
||||
|
||||
// Check that translations have not been modified by developers
|
||||
const translationAllowList = [
|
||||
"ElementBot",
|
||||
]
|
||||
|
||||
if (!translationAllowList.includes(user)) {
|
||||
if (editedFiles.some(file => file.endsWith("translations.xml"))) {
|
||||
fail("Some translation files have been edited. Only user `ElementBot` (i.e. translations coming from Localazy) is allowed to do that.\nPlease read more about translations management [in the doc](https://github.com/element-hq/element-x-android/blob/develop/CONTRIBUTING.md#strings).")
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package: name='io.element.android.x' versionCode='202609010' versionName='26.09.1' platformBuildVersionName='17' platformBuildVersionCode='37' compileSdkVersion='37' compileSdkVersionCodename='17'
|
||||
package: name='io.element.android.x' versionCode='202609020' versionName='26.09.2' platformBuildVersionName='17' platformBuildVersionCode='37' compileSdkVersion='37' compileSdkVersionCodename='17'
|
||||
sdkVersion:'24'
|
||||
targetSdkVersion:'37'
|
||||
uses-permission: name='android.permission.POST_NOTIFICATIONS'
|
||||
|
||||
Reference in New Issue
Block a user