diff --git a/.github/workflows/daedalus-docker.yml b/.github/workflows/daedalus-docker.yml index ed20f15ca9..84cec172a6 100644 --- a/.github/workflows/daedalus-docker.yml +++ b/.github/workflows/daedalus-docker.yml @@ -101,6 +101,10 @@ jobs: ~/.cargo/bin key: ${{ runner.os }}-${{ runner.arch }}-cargo-${{ hashFiles('**/Cargo.lock') }} + - name: Ensure sccache directory exists + if: needs.skip-if-clean.outputs.internal == 'true' + run: sudo mkdir -p /mnt/sccache && sudo chown $(id -u):$(id -g) /mnt/sccache + - name: Mount sccache disk cache if: needs.skip-if-clean.outputs.internal == 'true' uses: useblacksmith/stickydisk@13af8883542ca949a717e70fef89d15edbb29d88 # v1.2.0 diff --git a/.github/workflows/frontend-preview.yml b/.github/workflows/frontend-preview.yml index 0c9d5f2d20..b406d5bec4 100644 --- a/.github/workflows/frontend-preview.yml +++ b/.github/workflows/frontend-preview.yml @@ -66,6 +66,8 @@ jobs: - name: Build Storybook working-directory: ./packages/ui + env: + NODE_OPTIONS: --max-old-space-size=6144 run: pnpm run build-storybook - name: Configure short SHA @@ -76,8 +78,6 @@ jobs: - name: Deploy Storybook preview uses: cloudflare/wrangler-action@9acf94ace14e7dc412b076f2c5c20b8ce93c79cd # v3.15.0 - env: - NODE_OPTIONS: --max-old-space-size=6144 with: apiToken: ${{ secrets.CF_API_TOKEN }} accountId: ${{ secrets.CF_ACCOUNT_ID }} @@ -87,7 +87,11 @@ jobs: command: versions upload --preview-alias git-${{ steps.meta.outputs.sha_short }} comment: - if: github.repository_owner == 'modrinth' && github.event.pull_request.head.repo.full_name == github.repository + if: | + !cancelled() + && github.repository_owner == 'modrinth' + && github.event.pull_request.head.repo.full_name == github.repository + && needs.deploy.result == 'success' runs-on: ubuntu-latest needs: [deploy, deploy-storybook] steps: @@ -101,16 +105,25 @@ jobs: id: urls env: HEAD_SHA: ${{ github.event.pull_request.head.sha }} + STORYBOOK_RESULT: ${{ needs.deploy-storybook.result }} + RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} run: | STAGING_PREVIEW_URL=$(cat deployment-url-staging-preview.txt) PRODUCTION_PREVIEW_URL=$(cat deployment-url-production-preview.txt) + if [ "$STORYBOOK_RESULT" = "success" ]; then + STORYBOOK_PREVIEW_URL="https://git-${HEAD_SHA::8}-storybook.modrinth.workers.dev" + else + STORYBOOK_PREVIEW_URL="⚠️ Deployment $STORYBOOK_RESULT ([logs]($RUN_URL))" + fi + echo "Production preview URL: $PRODUCTION_PREVIEW_URL" echo "Staging preview URL: $STAGING_PREVIEW_URL" + echo "Storybook preview URL: $STORYBOOK_PREVIEW_URL" echo "staging-preview-url=$STAGING_PREVIEW_URL" >> $GITHUB_OUTPUT echo "production-preview-url=$PRODUCTION_PREVIEW_URL" >> $GITHUB_OUTPUT - echo "storybook-preview-url=https://git-${HEAD_SHA::8}-storybook.modrinth.workers.dev" >> $GITHUB_OUTPUT + echo "storybook-preview-url=$STORYBOOK_PREVIEW_URL" >> $GITHUB_OUTPUT - name: Find comment if: github.event_name == 'pull_request' diff --git a/.github/workflows/labrinth-docker.yml b/.github/workflows/labrinth-docker.yml index 58d5aa7081..cac802d660 100644 --- a/.github/workflows/labrinth-docker.yml +++ b/.github/workflows/labrinth-docker.yml @@ -109,6 +109,10 @@ jobs: ~/.cargo/bin key: ${{ runner.os }}-${{ runner.arch }}-cargo-${{ hashFiles('**/Cargo.lock') }} + - name: Ensure sccache directory exists + if: needs.skip-if-clean.outputs.internal == 'true' + run: sudo mkdir -p /mnt/sccache && sudo chown $(id -u):$(id -g) /mnt/sccache + - name: Mount sccache disk cache if: needs.skip-if-clean.outputs.internal == 'true' uses: useblacksmith/stickydisk@13af8883542ca949a717e70fef89d15edbb29d88 # v1.2.0 diff --git a/Cargo.lock b/Cargo.lock index 8d66b07bbb..eed207ff54 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2133,6 +2133,15 @@ dependencies = [ "cc", ] +[[package]] +name = "cobs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" +dependencies = [ + "thiserror 2.0.17", +] + [[package]] name = "color-eyre" version = "0.6.5" @@ -3259,6 +3268,18 @@ version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7" +[[package]] +name = "embedded-io" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced" + +[[package]] +name = "embedded-io" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" + [[package]] name = "encode_unicode" version = "1.0.0" @@ -3835,6 +3856,12 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" +[[package]] +name = "futures-timer" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af43fadb8a98512d547e37b4e92e0ced13e205c061b87b4623eff01d918d6968" + [[package]] name = "futures-util" version = "0.3.31" @@ -4146,6 +4173,29 @@ dependencies = [ "system-deps", ] +[[package]] +name = "governor" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9efcab3c1958580ff1f25a2a41be1668f7603d849bb63af523b208a3cc1223b8" +dependencies = [ + "cfg-if", + "dashmap", + "futures-sink", + "futures-timer", + "futures-util", + "getrandom 0.3.3", + "hashbrown 0.16.0", + "nonzero_ext", + "parking_lot", + "portable-atomic", + "quanta", + "rand 0.9.2", + "smallvec", + "spinning_top", + "web-time", +] + [[package]] name = "group" version = "0.13.0" @@ -5441,13 +5491,16 @@ dependencies = [ "json-patch 4.1.0", "labrinth", "lettre", + "lz4_flex", "meilisearch-sdk", "modrinth-content-management", "modrinth-util", "muralpay", "murmur2", + "neverbounce", "paste", "path-util", + "postcard", "prometheus", "quick-xml 0.38.3", "rand 0.8.5", @@ -6225,6 +6278,14 @@ dependencies = [ "jni-sys", ] +[[package]] +name = "neverbounce" +version = "0.1.0" +dependencies = [ + "reqwest 0.12.24", + "serde", +] + [[package]] name = "new_debug_unreachable" version = "1.0.6" @@ -6269,6 +6330,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "nonzero_ext" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38bf9645c8b145698bb0b18a4637dcacbc421ea49bef2317e4fd8065a387cf21" + [[package]] name = "noop_proc_macro" version = "0.3.0" @@ -7493,6 +7560,18 @@ version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" +[[package]] +name = "postcard" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24" +dependencies = [ + "cobs", + "embedded-io 0.4.0", + "embedded-io 0.6.1", + "serde", +] + [[package]] name = "potential_utf" version = "0.1.3" @@ -7857,6 +7936,21 @@ dependencies = [ "bytemuck", ] +[[package]] +name = "quanta" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3ab5a9d756f0d97bdc89019bd2e4ea098cf9cde50ee7564dde6b81ccc8f06c7" +dependencies = [ + "crossbeam-utils", + "libc", + "once_cell", + "raw-cpuid", + "wasi 0.11.1+wasi-snapshot-preview1", + "web-sys", + "winapi", +] + [[package]] name = "quartz_nbt" version = "0.2.9" @@ -8184,6 +8278,15 @@ dependencies = [ "rgb", ] +[[package]] +name = "raw-cpuid" +version = "11.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "498cd0dc59d73224351ee52a95fee0f1a617a2eae0e7d9d720cc622c73a54186" +dependencies = [ + "bitflags 2.9.4", +] + [[package]] name = "raw-window-handle" version = "0.6.2" @@ -9795,6 +9898,15 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d5fe4ccb98d9c292d56fec89a5e07da7fc4cf0dc11e156b41793132775d3e591" +[[package]] +name = "spinning_top" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d96d2d1d716fb500937168cc09353ffdc7a012be8475ac7308e1bdf0e3923300" +dependencies = [ + "lock_api", +] + [[package]] name = "spki" version = "0.7.3" @@ -10897,8 +11009,10 @@ dependencies = [ "fs4", "futures", "futures-lite 2.6.1", + "governor", "heck 0.5.0", "hickory-resolver 0.25.2", + "httpdate", "indicatif", "itertools 0.14.0", "modrinth-content-management", diff --git a/Cargo.toml b/Cargo.toml index 6ce8c1bc4d..2b49b71a2a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,7 @@ members = [ "packages/modrinth-log", "packages/modrinth-maxmind", "packages/modrinth-util", + "packages/neverbounce", "packages/path-util", ] @@ -89,10 +90,12 @@ fs4 = { version = "0.13.1", default-features = false } futures = "0.3.31" futures-lite = "2.6.1" futures-util = "0.3.31" +governor = "0.10.4" heck = "0.5.0" hex = "0.4.3" hickory-resolver = "0.25.2" hmac = "0.12.1" +httpdate = "1.0.3" hyper = "1.7.0" hyper-rustls = { version = "0.27.7", default-features = false, features = [ "aws-lc-rs", @@ -119,6 +122,12 @@ lettre = { version = "0.11.19", default-features = false, features = [ "tokio1", "tokio1-rustls", ] } +lz4_flex = { version = "0.11.5", default-features = false, features = [ + "checked-decode", + "safe-decode", + "safe-encode", + "std", +] } maxminddb = "0.26.0" meilisearch-sdk = { version = "0.30.0", default-features = false } modrinth-content-management = { path = "packages/modrinth-content-management" } @@ -127,6 +136,7 @@ modrinth-util = { path = "packages/modrinth-util" } muralpay = { path = "packages/muralpay" } murmur2 = "0.1.0" native-dialog = "0.9.2" +neverbounce = { path = "packages/neverbounce" } notify = { version = "8.2.0", default-features = false } notify-debouncer-mini = { version = "0.7.0", default-features = false } objc2-app-kit = { version = "0.3.2", default-features = false } @@ -136,6 +146,7 @@ paste = "1.0.15" path-util = { path = "packages/path-util" } phf = { version = "0.13.1", features = ["macros"] } png = "0.18.0" +postcard = { version = "1.1.3", default-features = false, features = ["alloc"] } proc-macro2 = { version = "1.0" } prometheus = "0.14.0" quartz_nbt = "0.2.9" diff --git a/_typos.toml b/_typos.toml index adee7387c6..74efbceb9b 100644 --- a/_typos.toml +++ b/_typos.toml @@ -8,8 +8,11 @@ extend-exclude = [ "packages/blog/", # contains licenses like `CC-BY-ND-4.0` "packages/moderation/src/data/stages/license.ts", + "packages/moderation/src/utils.ts", # contains payment card IDs like `IY1VMST1MOXS` which are flagged "apps/labrinth/src/queue/payouts/mod.rs", + # contains domain names with deliberate typos + "apps/labrinth/assets/disposable_email_blocklist.txt" ] [default.extend-words] diff --git a/apps/app-frontend/src/App.vue b/apps/app-frontend/src/App.vue index 7910ca37a7..90dd210d55 100644 --- a/apps/app-frontend/src/App.vue +++ b/apps/app-frontend/src/App.vue @@ -76,6 +76,7 @@ import FriendsList from '@/components/ui/friends/FriendsList.vue' import AddServerToInstanceModal from '@/components/ui/install_flow/AddServerToInstanceModal.vue' import UnknownPackWarningModal from '@/components/ui/install_flow/UnknownPackWarningModal.vue' import MinecraftAuthErrorModal from '@/components/ui/minecraft-auth-error-modal/MinecraftAuthErrorModal.vue' +import MinecraftRequiredModal from '@/components/ui/minecraft-required-modal/MinecraftRequiredModal.vue' import AppSettingsModal from '@/components/ui/modal/AppSettingsModal.vue' import AuthGrantFlowWaitModal from '@/components/ui/modal/AuthGrantFlowWaitModal.vue' import InstallToPlayModal from '@/components/ui/modal/InstallToPlayModal.vue' @@ -89,7 +90,14 @@ import SplashScreen from '@/components/ui/SplashScreen.vue' import WindowControls from '@/components/ui/WindowControls.vue' import { useCheckDisableMouseover } from '@/composables/macCssFix.js' import { config } from '@/config' -import { hide_ads_window, init_ads_window, show_ads_window } from '@/helpers/ads.js' +import { + ads_consent_listener, + get_ads_consent_required, + hide_ads_window, + init_ads_window, + perform_ads_consent_action, + show_ads_window, +} from '@/helpers/ads.js' import { debugAnalytics, initAnalytics, trackEvent } from '@/helpers/analytics' import { check_reachable } from '@/helpers/auth.js' import { get_user, get_version } from '@/helpers/cache.js' @@ -184,6 +192,8 @@ const { handleError, addNotification } = notificationManager const popupNotificationManager = new AppPopupNotificationManager() providePopupNotificationManager(popupNotificationManager) const { addPopupNotification } = popupNotificationManager +let adsConsentPopupId = null +let unlistenAdsConsent const appVersion = getVersion() const tauriApiClient = new TauriModrinthClient({ @@ -213,9 +223,20 @@ const { data: authenticatedModrinthUser } = useQuery({ enabled: () => !!credentials.value?.session, retry: false, }) +const hasPlus = computed( + () => + !!credentials.value?.user && + (hasMidasBadge(credentials.value.user) || + hasActivePride26Midas(authenticatedModrinthUser.value?.campaigns?.pride_26)), +) +const showAd = computed( + () => sidebarVisible.value && !hasPlus.value && credentials.value !== undefined, +) +const adConsentAvailable = computed(() => credentials.value !== undefined && !hasPlus.value) providePageContext({ hierarchicalSidebarAvailable: ref(true), - showAds: ref(false), + showAds: showAd, + adConsentAvailable, floatingActionBarOffsets: { left: ref(APP_LEFT_NAV_WIDTH), right: computed(() => (sidebarVisible.value ? `${APP_SIDEBAR_WIDTH}px` : '0px')), @@ -295,6 +316,12 @@ const authUnreachable = computed(() => { onMounted(async () => { await useCheckDisableMouseover() + try { + unlistenAdsConsent = await ads_consent_listener(handleAdsConsentRequired) + handleAdsConsentRequired(await get_ads_consent_required()) + } catch (error) { + handleError(error) + } document.querySelector('body').addEventListener('click', handleClick) document.querySelector('body').addEventListener('auxclick', handleAuxClick) @@ -308,6 +335,7 @@ onUnmounted(async () => { unsubscribeSidebarToggle() clearDelayedUpdatePopup() + await unlistenAdsConsent?.() await unlistenUpdateDownload?.() }) @@ -332,8 +360,77 @@ const messages = defineMessages({ defaultMessage: 'Minecraft authentication servers may be down right now. Check your internet connection and try again later.', }, + adsConsentTitle: { + id: 'app.ads-consent.title', + defaultMessage: 'Your privacy and how ads support Modrinth', + }, + adsConsentBody: { + id: 'app.ads-consent.body', + defaultMessage: + 'Ads make Modrinth possible and fund creator payouts. Our partners may store or access cookies in the app to personalize ads and measure performance.', + }, + adsConsentManage: { + id: 'app.ads-consent.manage', + defaultMessage: 'Manage preferences', + }, + adsConsentReject: { + id: 'app.ads-consent.reject', + defaultMessage: 'Reject all', + }, + adsConsentAccept: { + id: 'app.ads-consent.accept', + defaultMessage: 'Accept all', + }, }) +function handleAdsConsentRequired(required) { + if (!required) { + if (adsConsentPopupId !== null) { + popupNotificationManager.removeNotification(adsConsentPopupId) + adsConsentPopupId = null + } + return + } + + if ( + adsConsentPopupId !== null && + popupNotificationManager.getNotifications().some((item) => item.id === adsConsentPopupId) + ) { + return + } + + const notification = addPopupNotification({ + title: formatMessage(messages.adsConsentTitle), + text: formatMessage(messages.adsConsentBody), + type: 'info', + hideIcon: true, + autoCloseMs: null, + dismissible: false, + buttons: [ + { + label: formatMessage(messages.adsConsentManage), + action: () => perform_ads_consent_action('manage').catch(handleError), + color: 'standard', + keepOpen: true, + }, + { + label: formatMessage(messages.adsConsentReject), + action: () => perform_ads_consent_action('reject').catch(handleError), + color: 'brand', + keepOpen: true, + }, + { + label: formatMessage(messages.adsConsentAccept), + action: () => perform_ads_consent_action('accept').catch(handleError), + color: 'brand', + keepOpen: true, + }, + ], + }) + + adsConsentPopupId = notification.id +} + async function setupApp() { const { native_decorations, @@ -403,7 +500,7 @@ async function setupApp() { addNotification({ title: 'Warning', text: e.message, - type: 'warn', + type: 'warning', }), ) @@ -588,6 +685,7 @@ watch(stateInitialized, (ready) => { const error = useError() const errorModal = ref() const minecraftAuthErrorModal = ref() +const minecraftRequiredModal = ref() const contentInstall = createContentInstall({ router, handleError }) provideContentInstall(contentInstall) @@ -703,17 +801,6 @@ async function logOut() { await fetchCredentials() } -const hasPlus = computed( - () => - !!credentials.value?.user && - (hasMidasBadge(credentials.value.user) || - hasActivePride26Midas(authenticatedModrinthUser.value?.campaigns?.pride_26)), -) - -const showAd = computed( - () => sidebarVisible.value && !hasPlus.value && credentials.value !== undefined, -) - async function fetchIntercomToken() { const creds = await getCreds() if (!creds?.session) { @@ -740,19 +827,28 @@ async function fetchIntercomToken() { return await response.json() } -watch(showAd, () => { - if (!showAd.value) { - hide_ads_window(true) - } else { - init_ads_window(true) - } -}) +watch( + [showAd, adConsentAvailable], + async ([showAds, canManageConsent]) => { + if (showAds) { + await init_ads_window(true) + return + } + + await hide_ads_window(true) + if (canManageConsent) { + await init_ads_window() + } + }, + { immediate: true }, +) onMounted(() => { invoke('show_window') error.setErrorModal(errorModal.value) error.setMinecraftAuthErrorModal(minecraftAuthErrorModal.value) + error.setMinecraftRequiredModal(minecraftRequiredModal.value) setContentIncompatibilityWarningModal(incompatibilityWarningModal.value) setContentInstallModal(modInstallModal.value) @@ -1694,6 +1790,7 @@ provideAppUpdateDownloadProgress(appUpdateDownload) + -
-

- To play this instance, you must sign in through Microsoft below. If you don't have a - Minecraft account, you can purchase the game on the - Minecraft website. -

-
- -
-