Skip to content

Commit 6f04ab8

Browse files
committed
Merge remote-tracking branch 'upstream/main' into 65642-thread-back-navigation
2 parents 6a6b46b + dd11c95 commit 6f04ab8

480 files changed

Lines changed: 11949 additions & 12549 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/actions/javascript/proposalPoliceComment/index.js

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11639,6 +11639,7 @@ async function run() {
1163911639
console.log('commentsResponse', commentsResponse);
1164011640
core.endGroup();
1164111641
let didFindDuplicate = false;
11642+
let originalProposal;
1164211643
for (const previousProposal of commentsResponse) {
1164311644
const isProposal = !!previousProposal.body?.includes(CONST_1.default.PROPOSAL_KEYWORD);
1164411645
const previousProposalCreatedAt = new Date(previousProposal.created_at).getTime();
@@ -11664,13 +11665,14 @@ async function run() {
1166411665
if (similarityPercentage >= 90) {
1166511666
console.log(`Found duplicate with ${similarityPercentage}% similarity.`);
1166611667
didFindDuplicate = true;
11668+
originalProposal = previousProposal;
1166711669
break;
1166811670
}
1166911671
}
1167011672
}
1167111673
if (didFindDuplicate) {
1167211674
const duplicateCheckWithdrawMessage = proposalPolice_1.default.getDuplicateCheckWithdrawMessage();
11673-
const duplicateCheckNoticeMessage = proposalPolice_1.default.getDuplicateCheckNoticeMessage(newProposalAuthor);
11675+
const duplicateCheckNoticeMessage = proposalPolice_1.default.getDuplicateCheckNoticeMessage(newProposalAuthor, originalProposal?.html_url);
1167411676
// If a duplicate proposal is detected, update the comment to withdraw it
1167511677
console.log('ProposalPolice™ withdrawing duplicated proposal...');
1167611678
await GithubUtils_1.default.octokit.issues.updateComment({
@@ -12557,8 +12559,9 @@ const PROPOSAL_POLICE_TEMPLATES = {
1255712559
getDuplicateCheckWithdrawMessage: () => {
1255812560
return '#### 🚫 Duplicated proposal withdrawn by 🤖 ProposalPolice.';
1255912561
},
12560-
getDuplicateCheckNoticeMessage: (proposalAuthor) => {
12561-
return `⚠️ @${proposalAuthor} Your proposal is a duplicate of an already existing proposal and has been automatically withdrawn to prevent spam. Please review the existing proposals before submitting a new one.`;
12562+
getDuplicateCheckNoticeMessage: (proposalAuthor, originalProposalURL) => {
12563+
const existingProposalWithURL = originalProposalURL ? `[existing proposal](${originalProposalURL})` : 'existing proposal';
12564+
return `⚠️ @${proposalAuthor} Your proposal is a duplicate of an already ${existingProposalWithURL} and has been automatically withdrawn to prevent spam. Please review the existing proposals before submitting a new one.`;
1256212565
},
1256312566
};
1256412567
exports["default"] = PROPOSAL_POLICE_TEMPLATES;

.github/actions/javascript/proposalPoliceComment/proposalPoliceComment.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {context} from '@actions/github';
44
import type {IssueCommentCreatedEvent, IssueCommentEditedEvent, IssueCommentEvent} from '@octokit/webhooks-types';
55
import {format} from 'date-fns';
66
import {toZonedTime} from 'date-fns-tz';
7+
import type {TupleToUnion} from 'type-fest';
78
import {convertToNumber} from '@github/libs/ActionUtils';
89
import CONST from '@github/libs/CONST';
910
import GithubUtils from '@github/libs/GithubUtils';
@@ -91,6 +92,7 @@ async function run() {
9192
core.endGroup();
9293

9394
let didFindDuplicate = false;
95+
let originalProposal: TupleToUnion<typeof commentsResponse> | undefined;
9496
for (const previousProposal of commentsResponse) {
9597
const isProposal = !!previousProposal.body?.includes(CONST.PROPOSAL_KEYWORD);
9698
const previousProposalCreatedAt = new Date(previousProposal.created_at).getTime();
@@ -117,14 +119,15 @@ async function run() {
117119
if (similarityPercentage >= 90) {
118120
console.log(`Found duplicate with ${similarityPercentage}% similarity.`);
119121
didFindDuplicate = true;
122+
originalProposal = previousProposal;
120123
break;
121124
}
122125
}
123126
}
124127

125128
if (didFindDuplicate) {
126129
const duplicateCheckWithdrawMessage = PROPOSAL_POLICE_TEMPLATES.getDuplicateCheckWithdrawMessage();
127-
const duplicateCheckNoticeMessage = PROPOSAL_POLICE_TEMPLATES.getDuplicateCheckNoticeMessage(newProposalAuthor);
130+
const duplicateCheckNoticeMessage = PROPOSAL_POLICE_TEMPLATES.getDuplicateCheckNoticeMessage(newProposalAuthor, originalProposal?.html_url);
128131
// If a duplicate proposal is detected, update the comment to withdraw it
129132
console.log('ProposalPolice™ withdrawing duplicated proposal...');
130133
await GithubUtils.octokit.issues.updateComment({
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
#!/bin/bash
2+
3+
# Checks for usage of iframe and non-cloudflare URL for videos in documentation.
4+
# Assumes that Jekyll markdown documents compile and render correctly, i.e.
5+
# no malformed HTML tags or liquid tags.
6+
set -eu
7+
8+
source ./scripts/shellUtils.sh
9+
title "Enforce no iframe usage for videos and cloudflare CDN links"
10+
HAS_VIOLATION=false
11+
12+
# Use diff to find all changed markdown files in the docs/ directory compared
13+
# the current HEAD commit on main. This will mirror behavior on the workflow
14+
# when the local copy of origin/main is up-to-date. We exclude the README.md
15+
# files from linting.
16+
CHANGED_FILES="$(git diff origin/main..HEAD --name-only --diff-filter=MAR -- ':docs/*.md' ':(exclude,icase)*README.md')"
17+
18+
# RegEx to match the opening iframe tags.
19+
# Matches iframes like: <iframe src="https://myembeddedvideo.com" allowfullscreen width="10" height="10" allow="autoplay">
20+
# Broken into 2 consecutive non-capturing groups "(?:[...])".
21+
# Non-capturing groups are part of the matching pattern, but not returned in matches.
22+
# First Group: "<iframe[^>]*?"
23+
# - Checks for the iframe opening tag <iframe, and "[^>]*" non-greedily
24+
# consumes characters except for ">" (as few characters as possible
25+
# upto the closing tag).
26+
# Second Group: OR of three attribute tags "(\s*width|\s*height|\s*src)+"
27+
# - This "+" checks for at least one of the attributes in the group, in any order.
28+
# This is what we use to check if it's an embed.
29+
# - Each attribute tag consists of a named capturing group that saves quoted value
30+
# after the "=":
31+
# - [\"\"'](?<width>[^\"\"']+)[\"\"']
32+
# This matches the opening quotation marks followed by the named capturing
33+
# group (?<width>[....]) and the [^\"\"'] matches any character that isn't
34+
# a closing quotation mark. This doesn't allow for empty width attribute.
35+
# Followed by the closing quotation mark.
36+
# Finally, [^>]*? consumes all remaining characters after the attributes and
37+
# closes the iframe tag with ">". We consider there to be an iframe in the file
38+
# if all this is satisfied.
39+
REGEX="(?:<iframe[^>]*?)(?:\s*width=[\"\"'](?<width>[^\"\"']+)[\"\"']|\s*height=[\"\"'](?<height>[^'\"\"]+)[\"\"']|\s*src=[\"\"'](?<src>[^'\"\"]+[\"\"']))+[^>]*?>"
40+
while IFS= read -r FILE; do
41+
while IFS= read -r MATCH; do
42+
error "$FILE:$MATCH Do not use iframes for video embeds."
43+
HAS_VIOLATION=true
44+
done < <(pcregrep -n "$REGEX" "$FILE")
45+
done <<< "$CHANGED_FILES"
46+
47+
# RegEx to match liquid Jekyll tag for included videos, and extracts the src.
48+
# Matches includes like: {% include video.html src="https://incorrectlyembeddedvideo.com" %}
49+
#
50+
# The regex begins by checking for the opening "{% include video.html" with any
51+
# white spacing that wouldn't break the liquid tag. Followed by some white space.
52+
#
53+
# Next we have one non-capturing group "(?:[...])+" for attributes "thumbnail=..." and "src=....".
54+
# We expect at least one of these elements to appear.
55+
# Each attribute is formatted as: \s*ATTR=[\"\"'](?<ATTR>[^\"\"']+)[\"\"']
56+
# - This is zero or more whitespace followed by the opening "ATTR=", opening
57+
# quotes with non-empty string inside, and closing quotes. We capture the
58+
# value of the attribute using a named capturing group "(?<ATTR>[...])".
59+
# The attribute is closed then with some closing quotation marks "[\"\"']".
60+
# We close the regex with optional white space and ending %}. We extract
61+
REGEX="{%\s*include\s+video\.html\s+(?:\s*thumbnail=[\"\"'](?<thumbnail>[^'\"\"]+)[\"\"']|\s*src=[\"\"'](?<src>[^'\"\"]+[\"\"']))+\s*%}"
62+
63+
# RegEx to match a cloudflare CDN URL. Expects leading customer number and trailing content ID.
64+
# We match the "https://" followed by some subdomain "(?:\S+)" of alphanumeric-characters
65+
# usually customer information, then ".cloudflarestream.com/" and the optional trailing
66+
# remaining characters "(?:\S*)" usually content ID.
67+
CDN_REGEX="https:\/\/(?:\S+)\.cloudflarestream\.com\/(?:\S*)"
68+
while IFS= read -r FILE; do
69+
while IFS= read -r MATCH; do
70+
if ! echo "$MATCH" | pcregrep -q "$CDN_REGEX"; then
71+
error "$FILE:$MATCH Video URL must be from Cloudflare CDN."
72+
HAS_VIOLATION=true
73+
fi
74+
done < <(pcregrep -n "$REGEX" "$FILE")
75+
done <<< "$CHANGED_FILES"
76+
77+
if [[ $HAS_VIOLATION == true ]]; then
78+
error "Documentation has video violations"
79+
exit 1
80+
fi
81+
82+
success "No violations."

.github/workflows/deployExpensifyHelp.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ jobs:
3737
with:
3838
fetch-depth: 0
3939

40+
- name: Install pcregrep
41+
run: sudo apt-get install -y pcregrep
42+
4043
- name: Setup NodeJS
4144
uses: ./.github/actions/composite/setupNode
4245

@@ -48,6 +51,9 @@ jobs:
4851

4952
- name: Enforce that a redirect link has been created
5053
run: ./.github/scripts/enforceRedirect.sh
54+
55+
- name: Enforce iframe and Cloudflare CDN usage
56+
run: ./.github/scripts/enforceVideoFormats.sh
5157

5258
- name: Build with Jekyll
5359
uses: actions/jekyll-build-pages@0143c158f4fa0c5dcd99499a5d00859d79f70b0e

.github/workflows/presubmit.yml

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
name: PR Presubmit Reviews
2+
3+
permissions:
4+
contents: read
5+
pull-requests: write
6+
7+
on:
8+
pull_request:
9+
types: [opened, synchronize, labeled]
10+
pull_request_review_comment:
11+
types: [created]
12+
13+
jobs:
14+
review:
15+
runs-on: ubuntu-latest
16+
if: contains(github.event.pull_request.labels.*.name, 'AI Review')
17+
steps:
18+
- name: Checkout code
19+
uses: actions/checkout@v4
20+
21+
- name: Check required secrets
22+
run: |
23+
if [ -z "${{ secrets.LLM_API_KEY }}" ]; then
24+
echo "Error: LLM_API_KEY secret is not configured"
25+
exit 1
26+
fi
27+
28+
- name: Read RULES.md
29+
id: read-rules
30+
run: |
31+
RULES_CONTENT=$(cat "contributingGuides/review/RULES.md")
32+
{
33+
echo "content<<EOF"
34+
echo "$RULES_CONTENT"
35+
echo "EOF"
36+
} >> "$GITHUB_OUTPUT"
37+
38+
- name: Print RULES.md
39+
run: |
40+
echo "📋 RULES.md"
41+
echo "======================================"
42+
echo "${{ steps.read-rules.outputs.content }}"
43+
echo "======================================"
44+
45+
- uses: aldo-expensify/ai-reviewer-hackaton@c91b349b41efdf1eeaa290716628d76068148f0b
46+
env:
47+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
48+
LLM_API_KEY: ${{ secrets.LLM_API_KEY }}
49+
LLM_MODEL: "claude-3-7-sonnet-20250219"
50+
with:
51+
style_guide_rules: ${{ steps.read-rules.outputs.content }}

.github/workflows/unused-styles.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ on:
55
pull_request:
66
types: [opened, synchronize]
77
branches-ignore: [staging, production]
8-
paths: ['src', '.github/workflows/unused-styles.yml', 'scripts/findUnusedStyles.ts']
8+
paths: ['src/**', '.github/workflows/unused-styles.yml', 'scripts/findUnusedStyles.ts']
99

1010
concurrency:
1111
group: ${{ github.ref == 'refs/heads/main' && format('{0}-{1}', github.ref, github.sha) || github.ref }}-unused-styles

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,3 +153,6 @@ react-compiler-output.txt
153153

154154
# Generated by bob (for Nitro modules)
155155
modules/*/lib/
156+
157+
# Claude Code files
158+
.claude/

.storybook/preview.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import HTMLEngineProvider from '@src/components/HTMLEngineProvider';
1010
import {LocaleContextProvider} from '@src/components/LocaleContextProvider';
1111
import {EnvironmentProvider} from '@src/components/withEnvironment';
1212
import {KeyboardStateProvider} from '@src/components/withKeyboardState';
13+
import CONST from '@src/CONST';
14+
import IntlStore from '@src/languages/IntlStore';
1315
import ONYXKEYS from '@src/ONYXKEYS';
1416
import './fonts.css';
1517

@@ -20,6 +22,8 @@ Onyx.init({
2022
},
2123
});
2224

25+
IntlStore.load(CONST.LOCALES.EN);
26+
2327
const decorators = [
2428
(Story: React.ElementType) => (
2529
<ComposeProviders

Mobile-Expensify

README.md

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
* [canBeMissing onyx param](#canbemissing-onyx-param)
2525

2626
#### Additional Reading
27+
* [Application Philosophy](contributingGuides/philosophies/INDEX.md)
2728
* [API Details](contributingGuides/API.md)
2829
* [Offline First](contributingGuides/OFFLINE_UX.md)
2930
* [Contributing to Expensify](contributingGuides/CONTRIBUTING.md)
@@ -525,7 +526,7 @@ You can only build HybridApp if you have been granted access to [`Mobile-Expensi
525526
4. Run `git config --global submodule.recurse true` in order to have the submodule updated when you pull App.
526527

527528

528-
> [!Note]
529+
> [!Note]
529530
> #### For external agencies and C+ contributors only
530531
>
531532
> If you'd like to modify the `Mobile-Expensify` source code, it is best that you create your own fork. Then, you can swap origin of the remote repository by executing this command:
@@ -560,15 +561,15 @@ If for some reason, you need to target the standalone NewDot application, you ca
560561

561562
### Working with HybridApp vs Standalone NewDot
562563

563-
Day-to-day work with **HybridApp** shouldn't differ much from working on the standalone **NewDot** repository.
564-
The primary difference is that the native code, which runs React Native, is located in the following directories:
564+
Day-to-day work with **HybridApp** shouldn't differ much from working on the standalone **NewDot** repository.
565+
The primary difference is that the native code, which runs React Native, is located in the following directories:
565566
566567
- `./Mobile-Expensify/Android`
567568
- `./Mobile-Expensify/iOS`
568569
569570
### Important Notes:
570571
1. **Root Folders Do Not Affect HybridApp Builds:**
571-
- Changes made to the `./android` and `./ios` folders at the root of the repository **won't affect the HybridApp build**.
572+
- Changes made to the `./android` and `./ios` folders at the root of the repository **won't affect the HybridApp build**.
572573

573574
2. **Modifying iOS Code for HybridApp:**
574575
- If you need to remove `Pods`, you must do it in the **`./Mobile-Expensify/iOS`** directory.
@@ -583,23 +584,23 @@ The primary difference is that the native code, which runs React Native, is loca
583584
584585
### Updating the `Mobile-Expensify` Submodule
585586
586-
The `Mobile-Expensify` directory is a **Git submodule**. This means it points to a specific commit on the `Mobile-Expensify` repository.
587+
The `Mobile-Expensify` directory is a **Git submodule**. This means it points to a specific commit on the `Mobile-Expensify` repository.
587588
588-
If you'd like to fetch the submodule while executing the `git pull` command in `Expensify/App` instead of updating it manually you can run this command in the root of the project:
589+
If you'd like to fetch the submodule while executing the `git pull` command in `Expensify/App` instead of updating it manually you can run this command in the root of the project:
589590

590591
```
591592
git config submodule.recurse true
592593
```
593594

594-
> [!WARNING]
595+
> [!WARNING]
595596
> Please, remember that the submodule will get updated automatically only after executing the `git pull` command - if you switch between branches it is still recommended to execute `git submodule update` to make sure you're working on a compatible submodule version!
596597

597598
If you'd like to download the most recent changes from the `main` branch, please use the following command:
598599
```bash
599600
git submodule update --remote
600601
```
601602

602-
It's important to emphasize that a git submodule is just a **regular git repository** after all. It means that you can switch branches, pull the newest changes, and execute all regular git commands within the `Mobile-Expensify` directory.
603+
It's important to emphasize that a git submodule is just a **regular git repository** after all. It means that you can switch branches, pull the newest changes, and execute all regular git commands within the `Mobile-Expensify` directory.
603604

604605
### Adding HybridApp-related patches
605606

@@ -805,7 +806,7 @@ Some pointers:
805806
- When working with translations that involve plural forms, it's important to handle different cases correctly.
806807

807808
For example:
808-
- zero: Used when there are no items **(optional)**.
809+
- zero: Used when there are no items **(optional)**.
809810
- one: Used when there's exactly one item.
810811
- two: Used when there's two items. **(optional)**
811812
- few: Used for a small number of items **(optional)**.

0 commit comments

Comments
 (0)