30 Commits
Author SHA1 Message Date
devsyncwrldandClaude Opus 5 9b7511f8bf chore(ops): version the release scripts
CI / Build & test (Node 20) (push) Canceled after 0s
CI / Build & test (Node 24) (push) Canceled after 0s
CodeQL / Analyze (javascript-typescript) (push) Canceled after 0s
OpenSSF Scorecard / Scorecard analysis (push) Canceled after 0s
Security / Secret scan (gitleaks) (push) Canceled after 0s
Security / Dependency scan (OSV-Scanner) (push) Canceled after 0s
Security / IaC/config scan (Trivy) (push) Canceled after 0s
Security / License compliance scan (Trivy) (push) Canceled after 0s
CI / Build & test (push) Canceled after 0s
They existed only in /opt/ops on the VPS — no history, no backup. Rebuilding
that machine would have taken the whole publishing process with it, including
the Android signing flow written today.

/opt/ops now symlinks here so there is one copy rather than two that drift.
No secrets move: the scripts read tokens and the signing key from paths under
/root that stay on the VPS.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 20:21:00 -03:00
devsyncwrldandClaude Opus 5 cc2b8d3d33 feat(mobile): keep voice alive with the screen off
CI / Build & test (Node 20) (push) Canceled after 0s
CI / Build & test (Node 24) (push) Canceled after 0s
CI / Build & test (push) Canceled after 0s
CodeQL / Analyze (javascript-typescript) (push) Canceled after 0s
Security / Secret scan (gitleaks) (push) Canceled after 0s
Security / Dependency scan (OSV-Scanner) (push) Canceled after 0s
Security / IaC/config scan (Trivy) (push) Canceled after 0s
Security / License compliance scan (Trivy) (push) Canceled after 0s
OpenSSF Scorecard / Scorecard analysis (push) Canceled after 0s
A call dropped as soon as the screen locked. The cause is not configuration:
Android freezes background processes, and a frozen WebView stops running the
JavaScript that holds the LiveKit connection up. The only thing the system
accepts as "this app must keep running" is a foreground service, which is why
the persistent notification comes with it.

Two mechanisms, both needed: the service stops the process from being frozen,
and a partial wake lock stops the CPU from sleeping. Either one alone still
drops the call.

The notification carries a hangup action. The service cannot end the call
itself — only the web app knows how to leave a LiveKit room — so it notifies
the plugin's listeners and the web side disconnects through the same path the
in-app button uses.

The web side starts the service on mount when a call is already in progress,
not just on transition: the WebView can reload mid-call, and waiting for the
next connect would leave that call unprotected with no visible symptom until
someone locked their screen.

POST_NOTIFICATIONS is requested at launch on Android 13+. Denying it does not
drop calls — the service still runs — it only hides the notification.

Tests cover connect/disconnect, the already-in-a-call mount, the hangup action,
and that the component is inert in the browser and desktop builds. Verified by
mutation that dropping the start call fails the first one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 13:44:35 -03:00
devsyncwrldandClaude Opus 5 48e8bc537d ci(mobile): point setup-node at the mobile package's npm lockfile
Without cache-dependency-path the action falls back to the root package.json's
`packageManager: pnpm` field and fails looking for a pnpm this workflow never
installs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 13:11:46 -03:00
devsyncwrldandClaude Opus 5 f808f2f535 feat(mobile): Android app shell with Capacitor
The web client already has a full mobile UI (MobileShell and 17 sibling
screens) and ships as a PWA, so what was missing to get an APK was a native
container, not an interface.

The WebView loads the instance over the network instead of bundling a copy of
the web build. Bundling would mean republishing an APK on every site deploy —
there is no electron-updater equivalent here — and it would move the page to
https://localhost, turning every same-origin /api and /ws call into a
cross-origin one. The cost is no offline mode, which for a realtime chat app
costs nothing: public/index.html covers the unreachable-server case.

packages/mobile sits outside the pnpm workspace on purpose. Capacitor's CLI and
Gradle both assume a flat node_modules, and keeping it out leaves pnpm-lock.yaml
and the existing web/desktop CI untouched by it.

Icons are generated from the existing brand art. The launch screen is a
layer-list over the brand background rather than Capacitor's per-density
splash.png set, which is light and flashed white before the WebView painted.

CI builds an *unsigned* APK. The signing key stays on the VPS and is applied
there — an APK signature is what proves an update came from us, so the key has
no reason to exist on a second machine.

Background voice (foreground service) is not in this commit: a plain WebView is
throttled when the screen goes off and the call drops. That is the next phase.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 13:09:15 -03:00
devsyncwrldandClaude Opus 5 aee78c930d fix(desktop): announce staged updates reliably and install them silently
Two separate reasons "Restart to update" only ever showed up after pressing
"check for updates" by hand, and one reason the restart was unpleasant.

1. `update-downloaded` is a one-shot push to whichever page happens to be
   loaded. It is dropped when the window is in the tray (mainWindow is null),
   when the 10s post-launch check beats the web app's listener registration,
   and on every reload. Once an update is staged the poll latches off, so it
   was never re-announced — a manual check was the only way back. The main
   process already tracks this in recoveryStore, so expose it over IPC and have
   the banner ask on mount rather than rely on catching the event live.

2. UpdateBanner was only mounted in the mobile layout. On the desktop app the
   visible one was UpdateToast, which duplicated the same "update ready" card
   in untranslated English. Mount the banner on desktop too and reduce the
   toast to the failure case it does not share.

3. quitAndInstall() defaults to a non-silent install, so on Windows restarting
   meant clicking through the NSIS wizard. Pass silent + force-run-after, and
   switch the installer to one-click per-user, which is what lets the silent
   install proceed without an elevation prompt.

The manual-download link in the failure toast pointed at the upstream GitHub
repository, which is private for this fork and 404'd for everyone. Point it at
the fork's Gitea releases, where the updater feed already lives.

Regression test covers the case the live event never announced; verified by
mutation that removing the status query fails it and nothing else.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 12:26:30 -03:00
devsyncwrldandClaude Opus 5 5e7adfb38d chore: version 2.1.0
CI / Build & test (Node 20) (push) Canceled after 0s
CI / Build & test (Node 24) (push) Canceled after 0s
CI / Build & test (push) Canceled after 0s
CodeQL / Analyze (javascript-typescript) (push) Canceled after 0s
Security / Secret scan (gitleaks) (push) Canceled after 0s
Security / Dependency scan (OSV-Scanner) (push) Canceled after 0s
Security / IaC/config scan (Trivy) (push) Canceled after 0s
Security / License compliance scan (Trivy) (push) Canceled after 0s
OpenSSF Scorecard / Scorecard analysis (push) Canceled after 0s
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 10:09:30 -03:00
devsyncwrldandClaude Opus 5 033ba63762 feat(profile): banner repositioning, and the editor rebuilt on the reference's shape
CI / Build & test (Node 20) (push) Canceled after 0s
CI / Build & test (Node 24) (push) Canceled after 0s
CI / Build & test (push) Canceled after 0s
CodeQL / Analyze (javascript-typescript) (push) Canceled after 0s
Security / Secret scan (gitleaks) (push) Canceled after 0s
Security / Dependency scan (OSV-Scanner) (push) Canceled after 0s
Security / IaC/config scan (Trivy) (push) Canceled after 0s
Security / License compliance scan (Trivy) (push) Canceled after 0s
OpenSSF Scorecard / Scorecard analysis (push) Canceled after 0s
Three things, from the screenshots.

Banner repositioning, which was a real hole. A banner was always cropped dead
centre, and a GIF could not go through the still-image cropper at all — so
there was no way to frame one. The crop is now two percentages and a zoom
stored in the profile (`banner: {x, y, zoom}`), and the banner renders as an
<img> with object-position plus a scale rather than a CSS background: those two
compose into pan-and-zoom, where `background-size: cover` cannot be zoomed
without knowing the file's dimensions. The editor drags the live element, so a
GIF keeps animating while you frame it, and nothing is ever re-encoded.

The editor now has the reference's shape: a rail of cards on the left, each
showing what it currently holds, and the live profile beside it. A card opens a
dialog with the options on one side and a preview on the other. That is the
part both earlier attempts got wrong — the first put a 360px preview inside a
640px column, the second made you leave the preview to open a section. Here
they are on screen together at full size.

The dialog edits a copy: Cancel discards it, Apply commits to the draft, and
the draft is only persisted by Save. So backing out of a dialog cannot cost
unrelated work, and neither can the per-section reset in its header.

"Surprise me" randomises the look and nothing else. Fonts, effects, decoration,
theme and nameplate change; pronouns, tag, links, board and the banner crop are
the user's own words and framing, and rolling dice over those would be
vandalism. A test asserts exactly that over 40 rolls, and that every roll lands
on ids the normaliser accepts.

Five more nameplates in the y2k direction — holo, chrome, vapor, matrix, ember
— since the existing ten were all plain gradients.

Verified: shared build, web build, server typecheck, 523 web tests (5 new:
banner clamping and round-trip, randomiser bounds).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 10:02:29 -03:00
devsyncwrldandClaude Opus 5 e6eba7a2b7 fix(profile): align the decorations, and make the effects actually look like something
CI / Build & test (Node 20) (push) Canceled after 0s
CI / Build & test (Node 24) (push) Canceled after 0s
CI / Build & test (push) Canceled after 0s
CodeQL / Analyze (javascript-typescript) (push) Canceled after 0s
Security / Secret scan (gitleaks) (push) Canceled after 0s
Security / Dependency scan (OSV-Scanner) (push) Canceled after 0s
Security / IaC/config scan (Trivy) (push) Canceled after 0s
Security / License compliance scan (Trivy) (push) Canceled after 0s
OpenSSF Scorecard / Scorecard analysis (push) Canceled after 0s
Three complaints, three causes.

Decorations sat off-centre. <Avatar>'s box is `size + 2 × ringWidth`, but the
overlay was positioned with a percentage inset of that box — so it drifted by
exactly the ring whenever one was present. That is why the crown floated above
the head on the profile card while looking fine on the editor tiles, which
have no ring. DecoratedAvatar now takes `size` and `ringWidth` and computes
the inset in pixels, so the picture's edge always lands at a known radius in
the SVG viewBox and every decoration is drawn against it.

The ugly ones were ugly. Flames were a blob with a black circle punched out of
it; they are now fourteen tongues of alternating height on a gradient. The
crown's band sits ON the picture's top edge instead of hovering above it.
Frost, stars and hearts were placed by eye and are now on a computed ring.
Nine more: holo disc (a conic gradient — the honest way to draw one, and CSS
has it where SVG does not), chain, cyber brackets, lightning, spikes,
butterflies, sakura, bubbles, flower. Twenty in total.

Name effects were thin. Gradient was a two-stop ramp; it now has a lit middle.
Neon was one shadow, now five layered with a buzz. Glitch was a keyframed
text-shadow, now a real RGB split duplicated through ::before/::after from
data-text. Added chrome and gold (banded metal with a specular line — the y2k
look the reference is after), holo, rainbow, ice, 3D extrude, and outline.
Sixteen effects, thirteen fonts with Audiowide, Bungee and Monoton added.

The editor tiles now sample the user's own name rather than "Gg" or the effect
label: an effect reads differently on a short word than on the name it will
carry. Two per row instead of three, and section rows gained an icon.

One test failed on the catalog growth and was right to: it used 'rainbow' as
an example of an unknown id, and rainbow is now a real effect. It uses an
absurd id instead, and a new test walks every catalog id through the
normaliser so a catalog and the normaliser can never drift apart silently.

Verified: shared build, web build, server typecheck, 518 web tests (1 new).
Not verified: pixels — no browser here; the geometry is now computed rather
than guessed, but a screenshot decides.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 09:46:20 -03:00
devsyncwrldandClaude Fable 5.1 723522b90d fix(profile): give the editor room — three panels instead of a cramped column
CI / Build & test (Node 20) (push) Canceled after 0s
CI / Build & test (Node 24) (push) Canceled after 0s
CI / Build & test (push) Canceled after 0s
CodeQL / Analyze (javascript-typescript) (push) Canceled after 0s
Security / Secret scan (gitleaks) (push) Canceled after 0s
Security / Dependency scan (OSV-Scanner) (push) Canceled after 0s
Security / IaC/config scan (Trivy) (push) Canceled after 0s
Security / License compliance scan (Trivy) (push) Canceled after 0s
OpenSSF Scorecard / Scorecard analysis (push) Canceled after 0s
The owner's verdict on the first editor was "imprensado", and it was: the
settings shell caps its content at 640px, and the editor put a 360px preview
next to six-column tile grids inside that. Everything was legible and nothing
was usable.

Now it follows the shape of the reference (Discord's profile editor):

  - left, 320px: the list of sections, each showing its current value as a
    mini preview (the nameplate strip, the decorated avatar, the styled name,
    the theme swatch, the effect thumbnail). Picking one replaces the list
    with that section's editor, with a back arrow — a drill-down instead of
    sub-modals, one level deep.
  - centre: the live card, plus two in-context samples — how the name and tag
    read on a chat line, and the row in the member list with its nameplate.
  - right: the Board as others see it, with an Edit shortcut.

Tiles went from 5–6 per row to 2–3, with real size (Gg samples at 26px,
decorations on a 56px avatar, effect thumbnails 64px tall). The settings
shell drops its width cap and lets the panel fill the height for this tab
only; every other panel keeps the single reading column. Stacks to one
column below the lg breakpoint.

Verified: web build, 517 web tests. Not verified: pixels — there is no
browser here, so the proportions are from the reference and the numbers, and
a screenshot decides.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-03 09:27:38 -03:00
devsyncwrldandClaude Fable 5.1 e7094014b7 fix(shared): profile.ts must not reference the URL global type
CI / Build & test (Node 20) (push) Canceled after 0s
CI / Build & test (Node 24) (push) Canceled after 0s
CI / Build & test (push) Canceled after 0s
CodeQL / Analyze (javascript-typescript) (push) Canceled after 0s
Security / Secret scan (gitleaks) (push) Canceled after 0s
Security / Dependency scan (OSV-Scanner) (push) Canceled after 0s
Security / IaC/config scan (Trivy) (push) Canceled after 0s
Security / License compliance scan (Trivy) (push) Canceled after 0s
OpenSSF Scorecard / Scorecard analysis (push) Canceled after 0s
packages/shared compiles without the DOM or Node type libraries, so `URL` has
no type there even though it exists at runtime everywhere this code runs. The
web and server builds resolved the same file under their own libs and passed;
only the shared package's own `tsc` — which CI runs first — failed, on both
runners. Reach the constructor through globalThis with a minimal structural
type instead of pulling a lib into a types-only package. Runtime unchanged,
so the deployed site (82798d5c) needs no redeploy for this.

Lesson recorded for the local checklist: run `pnpm --filter @backspace/shared
build` too — the root `typecheck` script already does; the web build alone
does not.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-02 23:44:35 -03:00
devsyncwrldandClaude Fable 5.1 82798d5c5a chore: version 2.0.0
CI / Build & test (Node 20) (push) Canceled after 0s
CI / Build & test (Node 24) (push) Canceled after 0s
CI / Build & test (push) Canceled after 0s
CodeQL / Analyze (javascript-typescript) (push) Canceled after 0s
Security / Secret scan (gitleaks) (push) Canceled after 0s
Security / Dependency scan (OSV-Scanner) (push) Canceled after 0s
Security / IaC/config scan (Trivy) (push) Canceled after 0s
Security / License compliance scan (Trivy) (push) Canceled after 0s
OpenSSF Scorecard / Scorecard analysis (push) Canceled after 0s
The rebrand to Resenhacord moves the install and the profile directory;
a major bump marks that boundary for anyone reading the version string.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-02 23:38:30 -03:00
devsyncwrldandClaude Fable 5.1 ef9c73501c feat(profile): a full identity system — name styles, decorations, effects, nameplates, tag, links, board
CI / Build & test (Node 20) (push) Canceled after 0s
CI / Build & test (Node 24) (push) Canceled after 0s
CI / Build & test (push) Canceled after 0s
CodeQL / Analyze (javascript-typescript) (push) Canceled after 0s
Security / Secret scan (gitleaks) (push) Canceled after 0s
Security / Dependency scan (OSV-Scanner) (push) Canceled after 0s
Security / IaC/config scan (Trivy) (push) Canceled after 0s
Security / License compliance scan (Trivy) (push) Canceled after 0s
OpenSSF Scorecard / Scorecard analysis (push) Canceled after 0s
The owner asked for profile customisation that is "completo e FODA", pointing
at Discord's profile editor. This is the whole thing, end to end, with a live
preview.

What a user can now set, and where it shows:

  - Display name style: 10 fonts × 9 effects (solid, gradient, neon, toon,
    pop, gummy, prism, glitch, fire) × two colours. Popout and full profile.
  - Avatar decoration: 10 (ring, neon, flames, stars, halo, vines, pixel,
    hearts, frost, crown). Inline SVG — no image assets, scales to any size.
  - Profile effect over the banner: 9 (sparkles, rain, snow, matrix,
    confetti, aurora, embers, bubbles, hearts). CSS particles with seeded
    positions so re-renders do not change the weather.
  - Nameplate behind the member-list row: 10 presets + custom two-colour.
  - Theme: two colours washed behind the card, and the banner fallback.
  - Pronouns, and a clan-style tag (emoji + 2–4 letters) beside the name,
    also on the member row.
  - Links, up to 5, with the brand icon picked from the host.
  - Board: favourite game, up to 5 in rotation, up to 10 wanted, a quote.
    Games come from Steam search through a small server proxy (cached,
    10 min) or are typed by hand; covers only load from Steam's CDN.
  - Badges, derived rather than stored: instance admin, server owner,
    founder (joined in the space's first month), veteran (a year), OG (six
    months), stylist. Nothing to grant, revoke or migrate.

Storage is one JSON column, users.profile_style (migration 0019), and one
normaliser in shared/profile.ts that both the server (on write and on read)
and the client (on render) run. It never throws: unknown ids fall back,
strings are cut to their limits, colours must be #rrggbb, link URLs must be
https, cover URLs must be a Steam CDN host. Kept out of the federation
durable-field set on purpose — peers on older builds do not know the field,
and it is cosmetic.

The editor is a new Settings > Profile tab. Its preview is the same
ProfileCard component the popout renders, with the draft style swapped in —
what you see while editing is, by construction, what others see.

Fonts are OFL builds self-hosted from public/fonts alongside DM Sans, latin
subsets only, ~120 KB in total.

Verified: web build, 517 web tests (11 new: normaliser bounds and fallbacks,
link icon detection, badge derivation), server typecheck, 804 server tests.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-02 23:36:23 -03:00
devsyncwrldandClaude Fable 5.1 872066e5be feat: rebrand to Resenhacord with a new logo, and post a notice when a message is pinned
CI / Build & test (Node 20) (push) Canceled after 0s
CI / Build & test (Node 24) (push) Canceled after 0s
CI / Build & test (push) Canceled after 0s
CodeQL / Analyze (javascript-typescript) (push) Canceled after 0s
Security / Secret scan (gitleaks) (push) Canceled after 0s
Security / Dependency scan (OSV-Scanner) (push) Canceled after 0s
Security / IaC/config scan (Trivy) (push) Canceled after 0s
Security / License compliance scan (Trivy) (push) Canceled after 0s
OpenSSF Scorecard / Scorecard analysis (push) Canceled after 0s
Rebrand. The product name is now Resenhacord everywhere a person reads it:
window title, tray, notifications, About panel, PWA manifest, page title,
recovery and instance-picker screens, and the four i18n strings that named
the app. Code identifiers (BackspaceElectronAPI, @backspace/* workspace
names, the federation protocol) are unchanged — they are not a brand, and
renaming them buys nothing but churn. AGPL notices and upstream attribution
stay as they were.

Two rename details that would each have hurt real users:

  - electron-builder's productName changed but appId did not. NSIS keys the
    installation on appId, so the updater replaces the Backspace install in
    place instead of leaving a second app beside it.
  - userData is keyed on app name, so the rename would have moved every
    install to an empty profile — logged out, instance URL gone. main.ts now
    walks a migration chain ("@backspace/desktop" → "Backspace" →
    "Resenhacord"), each step a no-op once its source is gone, and one whose
    target already holds data leaves it alone. Autostart lookups accept the
    old registry entry name too.

Logo. Two speech bubbles in conversation, violet behind, rose in front with
three dots — the front one is talking. Sources are hand-written SVG in
assets/brand (square viewBox; the gap between bubbles is a mask, not paint,
so the alpha-only macOS template keeps the silhouette). PNG sources are
rendered from the icon SVG and the whole set — .icns, .ico, Linux PNGs, tray
variants, favicons, PWA, maskable — comes from the existing gen-icons
pipeline unchanged.

Pin notice. Pinning a message now posts "X pinned a message to this channel.
See all pinned messages." into the channel, the way Discord does, with two
links: the message itself (jumps to it) and the pins panel. Space messages
had no `type` column — only DM messages carried system events — so this adds
one (migration 0018, default 'user', verified against a copy of the
production database: 88 rows, all 'user'). The route inserts a type='system'
row alongside the pin and broadcasts it as message_created, so the client
renders it with the SystemMessage renderer it already has. Unpinning posts
nothing; it is housekeeping, not news. System notices are excluded from the
notification toast and the message sound — they are not from anyone.

The pins panel and jump target are local state in MainContent; a notice deep
in the timeline reaches them through two small uiStore requests (a counter
and an id), neither persisted.

Verified: web build, 506 web tests (2 new), server typecheck, 804 server
tests, desktop typecheck.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-02 23:15:14 -03:00
devsyncwrldandClaude Opus 5 a41a944742 feat(chat): @everyone mention
CI / Build & test (Node 20) (push) Canceled after 0s
CI / Build & test (Node 24) (push) Canceled after 0s
CI / Build & test (push) Canceled after 0s
CodeQL / Analyze (javascript-typescript) (push) Canceled after 0s
Security / Secret scan (gitleaks) (push) Canceled after 0s
Security / Dependency scan (OSV-Scanner) (push) Canceled after 0s
Security / IaC/config scan (Trivy) (push) Canceled after 0s
Security / License compliance scan (Trivy) (push) Canceled after 0s
OpenSSF Scorecard / Scorecard analysis (push) Canceled after 0s
Rides the existing `<@id>` token: `everyone` already matches the parser's
`[a-zA-Z0-9_-]+`, so the markdown pre-processor, the inline renderer and the
badge pick it up with no second syntax. User ids are snowflakes, so the
literal `everyone` cannot collide with one.

Gated on a new MENTION_EVERYONE permission rather than being open to all —
otherwise a role can never take it away. Granted to @everyone by default: on
a small instance the ping is a convenience, not a weapon, and an owner who
disagrees can deny it and grant it to a role instead.

Two things that would each have made the gate a no-op:

  - DEFAULT_EVERYONE_PERMISSIONS only applies at space creation, so spaces
    that predate the bit would have had every @everyone mention silently
    stripped. A boot-time backfill ORs the bit into @everyone roles only
    (`id = space_id`), leaving other roles alone so a later denial is not
    overruled on the next restart. Verified against a copy of the production
    database: three roles moved by exactly 2^15, non-@everyone roles
    untouched, second pass a no-op.
  - Enforcement covers edit as well as create; without that, posting a plain
    message and editing it is the way around the permission.

A message that lacks the permission is not rejected — losing what someone
wrote over one token is worse than posting it. The token is demoted to the
literal `@everyone` text they typed: the message reads the same and rings
nobody.

Code spans and fences never ping. People paste payloads and snippets into
chat, and a paste should not ring everyone's client. The same rule decides
the row highlight and the notification sound, so the three cannot disagree.

Verified: web build, server typecheck, 504 web tests, 804 server tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-02 22:10:56 -03:00
devsyncwrldandClaude Opus 5 1ec5d88fa2 chore: version 1.5.0
CI / Build & test (Node 20) (push) Canceled after 0s
CI / Build & test (Node 24) (push) Canceled after 0s
CI / Build & test (push) Canceled after 0s
CodeQL / Analyze (javascript-typescript) (push) Canceled after 0s
Security / Secret scan (gitleaks) (push) Canceled after 0s
Security / Dependency scan (OSV-Scanner) (push) Canceled after 0s
Security / IaC/config scan (Trivy) (push) Canceled after 0s
Security / License compliance scan (Trivy) (push) Canceled after 0s
OpenSSF Scorecard / Scorecard analysis (push) Canceled after 0s
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-02 21:58:05 -03:00
devsyncwrldandClaude Opus 5 699855849d feat(i18n): translate the five systems users actually see
CI / Build & test (Node 20) (push) Canceled after 0s
CI / Build & test (Node 24) (push) Canceled after 0s
CI / Build & test (push) Canceled after 0s
CodeQL / Analyze (javascript-typescript) (push) Canceled after 0s
Security / Secret scan (gitleaks) (push) Canceled after 0s
Security / Dependency scan (OSV-Scanner) (push) Canceled after 0s
Security / IaC/config scan (Trivy) (push) Canceled after 0s
Security / License compliance scan (Trivy) (push) Canceled after 0s
OpenSSF Scorecard / Scorecard analysis (push) Canceled after 0s
Ordered by what the group hits, not by string count.

  - Chat system messages ("X added Y to the group") and their sidebar
    preview twin. These are content, not chrome, so they sat in English in
    the middle of conversations.
  - Login, register and the invite landing page — the first screen anyone
    invited to the instance sees.
  - Voice — controls, connection info, the mobile join sheet, settings tab.
  - Mobile screens.
  - Settings navigation across space, user and instance, plus Discovery and
    join requests.

Three call sites build strings outside a component and so read the locale
directly rather than through the hook: the sidebar preview formatter, the
connection-info track labels, and MobileShell's screen map — which holds
plain functions that are *called*, not rendered, where a hook would run
under whichever component invoked the map. Each says so at the definition;
all three re-render often enough that a language switch shows through
promptly.

Adds a test for a mistake made three times in this series: a file with an
exported component plus a private sub-component, where only the exported one
got the hook. tsc catches it, but only after a full build and without naming
the component. The test names it, and also flags the inverse — a hook placed
below an early return, which tsc accepts and React breaks at runtime (this
project shipped that once, as error #310). Verified by mutation: removing a
hook makes it fail and print the component.

Dictionary is at 856 keys, en and pt-BR at exact parity.

Verified: web build (tsc + vite), 492 web tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-02 21:13:54 -03:00
devsyncwrldandClaude Opus 5 94f5da4013 feat(i18n): translate five more systems, and unify the permission labels
CI / Build & test (Node 20) (push) Canceled after 0s
CI / Build & test (Node 24) (push) Canceled after 0s
CI / Build & test (push) Canceled after 0s
CodeQL / Analyze (javascript-typescript) (push) Canceled after 0s
Security / Secret scan (gitleaks) (push) Canceled after 0s
Security / Dependency scan (OSV-Scanner) (push) Canceled after 0s
Security / IaC/config scan (Trivy) (push) Canceled after 0s
Security / License compliance scan (Trivy) (push) Canceled after 0s
OpenSSF Scorecard / Scorecard analysis (push) Canceled after 0s
- Space settings > Overview — identity, icon, banner, transfer, delete
  - Space settings > Members and Bans
  - Space settings > Roles, including the whole permission table
  - Category settings — rename, privacy, overrides, delete
  - Instance settings > General — name, discovery, GIF search

The permission labels were duplicated: the roles panel carried one English
list and the channel/category override editors carried another. Both now
read a single `perm.*` set, so renaming a permission lands in all three at
once instead of one and a half. This removes the `channel.perm.*` keys added
two commits ago rather than adding a third copy beside them.

Same pattern as before for anything chosen outside the render: the roles
panel's permission groups and the category's override definitions hold keys
and resolve them where they render. The category delete dialog emphasises
the category name, so its sentence is split around the placeholder rather
than interpolated.

Dictionary is at 723 keys, en and pt-BR at exact parity.

Verified: web build (tsc + vite), 490 web tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-02 20:49:25 -03:00
devsyncwrldandClaude Opus 5 4fd6ac890c feat(i18n): translate five more systems
CI / Build & test (Node 20) (push) Canceled after 0s
CI / Build & test (Node 24) (push) Canceled after 0s
CI / Build & test (push) Canceled after 0s
CodeQL / Analyze (javascript-typescript) (push) Canceled after 0s
Security / Secret scan (gitleaks) (push) Canceled after 0s
Security / Dependency scan (OSV-Scanner) (push) Canceled after 0s
Security / IaC/config scan (Trivy) (push) Canceled after 0s
Security / License compliance scan (Trivy) (push) Canceled after 0s
OpenSSF Scorecard / Scorecard analysis (push) Canceled after 0s
- Settings > Voice: video — preview, permission states, camera picker.
    This closes the Voice & Video tab; input, output and video are all done.
  - Create channel / category / space
  - Group DMs — new DM, add friends, group settings, leave
  - Join a space — discovery, invite code, and both federated auth phases
  - Delete account, transfer ownership, and the top-level error screen

Three patterns worth noting. Strings that are chosen in a helper or held in
state (the camera's hard-blocked copy, its preview errors, the space
visibility options, the invite failure reasons) now travel as translation
keys and are resolved where they render, so switching language re-labels
what is already on screen. Sentences that wrap an emphasised literal — the
host name when joining, the space and member names when transferring — are
split around their placeholder instead of interpolated, leaving word order
to the translation. And `CreateChannel` mapped over `(t) =>` for the channel
type, which shadowed the translator; the loop variable is now `channelType`.

The error boundary in main.tsx is a class component, so it reads the locale
once per render via `translate` rather than the hook — correct here anyway,
since an error screen has no reason to re-render on a language switch.

Two GroupDmSettings tests located the name input by its aria-label, which
this change translates. They now use the `data-group-dm-name-input`
attribute the input already carries, matching how the rest of that file
locates elements — and no longer break when copy changes.

Dictionary is at 595 keys, en and pt-BR at exact parity.

Verified: web build (tsc + vite), 490 web tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-02 20:38:54 -03:00
devsyncwrldandClaude Opus 5 115c81cba3 feat(i18n): translate five more systems, and check for updates more often
CI / Build & test (Node 20) (push) Canceled after 0s
CI / Build & test (Node 24) (push) Canceled after 0s
CI / Build & test (push) Canceled after 0s
CodeQL / Analyze (javascript-typescript) (push) Canceled after 0s
Security / Secret scan (gitleaks) (push) Canceled after 0s
Security / Dependency scan (OSV-Scanner) (push) Canceled after 0s
Security / IaC/config scan (Trivy) (push) Canceled after 0s
Security / License compliance scan (Trivy) (push) Canceled after 0s
OpenSSF Scorecard / Scorecard analysis (push) Canceled after 0s
Update cadence: the desktop app checked once ten seconds after launch and
then every four hours. A release published while the app sits open went
unnoticed until the next poll — which is exactly what happened with 1.4.0.
It now polls every 30 minutes and also checks when the window regains focus,
throttled to one check per five minutes so alt-tabbing does not hammer the
feed. `lastCheckAt` is stamped from the `checking-for-update` event rather
than by the callers, so a manual check from the settings panel counts too.
The focus listener is on `app`, not on mainWindow, because the window is
destroyed and recreated when closed to tray.

Translations, five whole systems:
  - Settings > Desktop (autostart, updates, instance)
  - Settings > Voice: output device, which completes that tab
  - Settings > Keybinds, including the action names and conflict prompt
  - Settings > Account, the largest — profile, password, detached-instance
    notice, danger zone
  - Explore spaces

Two sentences wrap a literal that must not be translated (the `input` group
name, the `user@instance` example). Those are split around a placeholder
rather than interpolated as text, so a translation keeps control of the word
order on both sides of it. Keybind action labels gained a `labelKey`
alongside the English `label`, which stays for non-React callers.

Verified: web build (tsc + vite), desktop typecheck, 490 web tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-02 13:38:28 -03:00
devsyncwrldandClaude Opus 5 a54d998ba9 chore: version 1.4.0
CI / Build & test (Node 20) (push) Canceled after 0s
CI / Build & test (Node 24) (push) Canceled after 0s
CI / Build & test (push) Canceled after 0s
CodeQL / Analyze (javascript-typescript) (push) Canceled after 0s
Security / Secret scan (gitleaks) (push) Canceled after 0s
Security / Dependency scan (OSV-Scanner) (push) Canceled after 0s
Security / IaC/config scan (Trivy) (push) Canceled after 0s
Security / License compliance scan (Trivy) (push) Canceled after 0s
OpenSSF Scorecard / Scorecard analysis (push) Canceled after 0s
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-02 12:52:31 -03:00
devsyncwrldandClaude Opus 5 860f02b2c1 feat(channels): rename channels, and quieter defaults for sound and streams
CI / Build & test (Node 20) (push) Canceled after 0s
CI / Build & test (Node 24) (push) Canceled after 0s
CI / Build & test (push) Canceled after 0s
CodeQL / Analyze (javascript-typescript) (push) Canceled after 0s
Security / Secret scan (gitleaks) (push) Canceled after 0s
Security / Dependency scan (OSV-Scanner) (push) Canceled after 0s
Security / IaC/config scan (Trivy) (push) Canceled after 0s
Security / License compliance scan (Trivy) (push) Canceled after 0s
OpenSSF Scorecard / Scorecard analysis (push) Canceled after 0s
Four owner requests in one pass.

Rename: the PATCH /api/channels/:id route already accepted a name and a
topic, and the API client already exposed it — the only thing missing was a
way to reach it. The channel settings Overview tab now edits both, gated on
MANAGE_CHANNELS like the server is, with Save/Discard appearing only when
the form is dirty. The server's lowercase-and-hyphenate rule moved to
@backspace/shared so the field can show what a name will actually become
instead of letting it change shape on save; one copy of the rule, used by
both sides.

Stream volume: someone else's screen share now starts at 50 instead of 100
(DEFAULT_STREAM_VOLUME). The publisher boosts system audio before sending,
so the old default arrived far too loud for a viewer who never touched the
slider. Only the default moves — the slider still covers the full range and
a value the user set is still persisted.

Sound effects: SFX_BASE_VOLUME 0.8 -> 0.4, halving every cue. The .ogg files
were left alone; they already peak below full scale, so the harshness was
level, not recorded distortion. Every SFX playback path goes through
getSfxVolume(), so this is the single knob.

Translations: channel settings (including the override editor's permission
labels), the friends page end to end — tabs, lists, requests, the add tab
and discovery cards — the invite modal including its per-invite failure
reasons, and the group-DM member actions. Failure reasons are carried as
translation keys rather than finished text, so results already on screen
re-label when the language changes.

Verified: web build (tsc + vite), server typecheck, 490 web tests, 804
server tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-02 11:59:25 -03:00
devsyncwrldandClaude Opus 5 293378293d fix(ci): match release assets by name, and close the guard block
CI / Build & test (Node 20) (push) Canceled after 0s
CI / Build & test (Node 24) (push) Canceled after 0s
CI / Build & test (push) Canceled after 0s
CodeQL / Analyze (javascript-typescript) (push) Canceled after 0s
Security / Secret scan (gitleaks) (push) Canceled after 0s
Security / Dependency scan (OSV-Scanner) (push) Canceled after 0s
Security / IaC/config scan (Trivy) (push) Canceled after 0s
Security / License compliance scan (Trivy) (push) Canceled after 0s
OpenSSF Scorecard / Scorecard analysis (push) Canceled after 0s
Two defects, both mine, and the second hid the first.

The cleanup built a regex and matched it against the jq line '<id> <name>', so
the anchor in latest.yml could never match — it sits after the id. Only the
.exe was removed, and each publish left another latest.yml behind. Gitea then
served the older one, so the updater kept reporting the previous version as
current: an update that exists and is never offered, with nothing logged
anywhere. Filtering now happens inside jq, comparing the name directly.

The guard meant to catch exactly that was missing its closing fi, so the step
died on a syntax error before reaching it. Verified with bash -n this time,
which is what should have happened before it ever ran on a runner.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-01 19:14:59 -03:00
devsyncwrldandClaude Opus 5 c5abb6fd64 chore: version 1.3.0
CI / Build & test (Node 20) (push) Canceled after 0s
CI / Build & test (Node 24) (push) Canceled after 0s
CI / Build & test (push) Canceled after 0s
CodeQL / Analyze (javascript-typescript) (push) Canceled after 0s
Security / Secret scan (gitleaks) (push) Canceled after 0s
Security / Dependency scan (OSV-Scanner) (push) Canceled after 0s
Security / IaC/config scan (Trivy) (push) Canceled after 0s
Security / License compliance scan (Trivy) (push) Canceled after 0s
OpenSSF Scorecard / Scorecard analysis (push) Canceled after 0s
Carries the silent native notification, which lives in the Electron main
process and therefore only takes effect from a new installer — the reason this
release is needed beyond exercising the restart prompt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-01 19:07:26 -03:00
devsyncwrldandClaude Opus 5 1fb61377b9 feat(notify): in-app notifications with the app's own sound
CI / Build & test (Node 20) (push) Canceled after 0s
CI / Build & test (Node 24) (push) Canceled after 0s
CI / Build & test (push) Canceled after 0s
CodeQL / Analyze (javascript-typescript) (push) Canceled after 0s
Security / Secret scan (gitleaks) (push) Canceled after 0s
Security / Dependency scan (OSV-Scanner) (push) Canceled after 0s
Security / IaC/config scan (Trivy) (push) Canceled after 0s
Security / License compliance scan (Trivy) (push) Canceled after 0s
OpenSSF Scorecard / Scorecard analysis (push) Canceled after 0s
The system balloon carries the OS notification sound, which does not belong to
this app, and it only fired while the window was out of focus — with the app
focused nothing appeared at all.

Notifications now surface inside the window, carry the same synthesised timbre
as the rest of the app's sounds, and clicking one opens the channel. The native
balloon is kept for when the window is not visible, since an in-app card
nobody can see is no notification, but it is now silent: the app plays its own
effect instead.

A focused window is notified only about other channels — announcing the
conversation someone is already reading is noise.

Also fixes the Gitea publish cleanup, which silently deleted nothing: it
interpolated an Actions expression inside a bash , and when the pattern
did not match, the loop passed over every asset. The release ended with two
latest.yml files and the updater served the older one, reporting 1.1.0 as
current — an update that exists but is never offered, with no error anywhere.
The filter is plain bash now, logs what it found, and the job fails if more
than one latest.yml survives.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-01 18:57:02 -03:00
devsyncwrldandClaude Opus 5 1a1067bbb6 chore: version 1.2.0
CI / Build & test (Node 20) (push) Canceled after 0s
CI / Build & test (Node 24) (push) Canceled after 0s
CI / Build & test (push) Canceled after 0s
CodeQL / Analyze (javascript-typescript) (push) Canceled after 0s
Security / Secret scan (gitleaks) (push) Canceled after 0s
Security / Dependency scan (OSV-Scanner) (push) Canceled after 0s
Security / IaC/config scan (Trivy) (push) Canceled after 0s
Security / License compliance scan (Trivy) (push) Canceled after 0s
OpenSSF Scorecard / Scorecard analysis (push) Canceled after 0s
Pins, filtered search, custom emojis and stickers, and the in-app restart
prompt. The prompt itself is only visible once an update is offered, so this
release is what makes it observable.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-01 18:49:23 -03:00
devsyncwrldandClaude Opus 5 71d2109b2f feat(desktop): in-app prompt to restart for an update
CI / Build & test (Node 20) (push) Canceled after 0s
CI / Build & test (Node 24) (push) Canceled after 0s
CI / Build & test (push) Canceled after 0s
CodeQL / Analyze (javascript-typescript) (push) Canceled after 0s
Security / Secret scan (gitleaks) (push) Canceled after 0s
Security / Dependency scan (OSV-Scanner) (push) Canceled after 0s
Security / IaC/config scan (Trivy) (push) Canceled after 0s
Security / License compliance scan (Trivy) (push) Canceled after 0s
OpenSSF Scorecard / Scorecard analysis (push) Canceled after 0s
The main process already showed a system notification when an update finished
downloading, and nothing in the renderer listened — so the only cue vanished on
its own and was missed by anyone not watching the desktop at that moment.

A prompt now stays in the window until it is acted on or dismissed, with the
restart wired to the install-update channel the preload already exposed.

Dismissing is 'not now', not 'never': a later version reopens the prompt, since
the version it refers to has changed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-01 18:31:59 -03:00
devsyncwrldandClaude Opus 5 7f08384372 feat(expressions): pickers and upload for emojis and stickers
Completes the feature: the tables existed but nothing could be put in them.

Space settings gain an Emojis & Stickers panel behind MANAGE_SPACE, with a
512KB ceiling — both are fetched on every message that uses them, so weight
matters more than fidelity. The suggested name is pre-normalised so the common
case needs no typing, and a name collision reports itself distinctly from an
upload failure: the corrective action is different.

Custom emojis join the emoji picker as their own category. They have no native
character, so selecting one inserts :name: — the same text the renderer
resolves back to an image, which also means copying a message yields something
that still reads.

Stickers get a picker tab that only appears inside a space, since that is where
they exist, and send immediately on click: a sticker is the whole message, so
parking it in the composer to await Enter would make no sense.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-01 18:25:10 -03:00
devsyncwrldandClaude Opus 5 7d003021d1 feat(expressions): custom emojis and stickers per space
Emojis and stickers get separate tables on purpose: an emoji sits inside a
sentence and a sticker is the whole message, so their sizes, limits and render
paths differ.

Emoji names are unique per space and restricted to letters, digits and
underscore — :name: has to resolve to one image, and a space or colon inside
the name would make the reference impossible to delimit. Sticker names are
labels picked from a grid, so they accept anything.

Rendering reuses the existing markdown pipeline by rewriting :name: into image
syntax, which needs no plugin. Code spans and fences are left alone: text
someone wrapped in backticks was meant to be shown literally, and turning it
into an image would destroy what they wrote.

Messages carry stickerId with ON DELETE set null — removing a sticker from the
space must not delete the history of everyone who used it. The server refuses a
sticker from another space, which would otherwise leak images between unrelated
servers.

Expressions load once per space and stay in memory: the message renderer
consults the map for every :name: it finds, and a request per lookup would turn
each message into a cascade.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-01 17:57:32 -03:00
devsyncwrldandClaude Opus 5 50a8f12c77 feat(search): inline filters, and translate the search UI
The filters themselves already existed end to end — the server takes q, from,
has, before and after, the API client passes them, and the popover has fields
for each. What it lacked was discovery: the panel sits behind a button, so the
capability was invisible.

Typing 'de:fulano' or 'has:image' straight into the search box now applies the
same filters. Keys are accepted in both languages, since the app is bilingual,
and an unrecognised token falls back to being search text — otherwise a message
containing a URL or 'algo:coisa' would become unfindable.

Inline filters win over the panel's: whoever just typed one is expressing the
more recent intent.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-01 17:49:05 -03:00
devsyncwrldandClaude Opus 5 4ceb5cd66a feat(pins): pin messages to a channel
Pinned state lives on the message rather than a join table: a message is
pinned in exactly one channel, its own, so a separate table would add a join to
every lookup and buy nothing.

Every message now carries its pin state, so the timeline can mark a pin without
a second request and the panel and the timeline cannot disagree. Toggling is
deliberately not optimistic — the server refuses past the channel's limit, and
showing it pinned before confirmation would lie in exactly that case.

The pins list reuses the same assembly the channel history uses, extracted into
one helper, so the two cannot drift apart in what they include.

The migration also adds the (channel, user, created) index the filtered search
will need, since both touch the same table and one migration is cheaper than
two.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-01 17:45:33 -03:00
254 changed files with 29527 additions and 1269 deletions
+92
View File
@@ -0,0 +1,92 @@
# Compila o APK do Android no GitHub e o devolve como artefato.
#
# O runner entra só como máquina de build, pelo mesmo motivo do desktop: a VPS
# tem ~2 GB de RAM livres e roda a produção, então um Gradle com o SDK do
# Android ali dentro arrisca derrubar as chamadas de quem está usando o app.
#
# O APK sai **sem assinatura**. A chave de assinatura fica na VPS e não sobe
# para cá: quem assina é /opt/ops/release-android.sh, depois de baixar este
# artefato. Um APK assinado é instalável por qualquer um, e a chave é o que
# prova que uma atualização veio de nós — ela não tem por que existir em duas
# máquinas.
name: Android APK
on:
workflow_dispatch:
push:
tags: ['android-v*']
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
- name: Setup Java
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '21'
# cache/cache-dependency-path apontando para o npm deste pacote não é só
# otimização: sem eles o setup-node lê o campo `packageManager` do
# package.json da raiz, tenta usar pnpm — que este workflow não instala —
# e falha antes de rodar qualquer coisa.
- name: Setup Node.js
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
with:
node-version: 22
cache: npm
cache-dependency-path: packages/mobile/package-lock.json
# npm, não pnpm: packages/mobile fica fora do workspace de propósito
# (ver pnpm-workspace.yaml). Ele tem o próprio package-lock.json.
- name: Install Capacitor
working-directory: packages/mobile
run: npm ci
# Copia a config e os arquivos de public/ para dentro do projeto nativo.
# Sem isso o APK sai com o que estiver commitado em assets/, que pode
# estar velho — esses arquivos são gerados, não versionados.
- name: Sync Capacitor
working-directory: packages/mobile
run: npx cap sync android
- name: Cache Gradle
uses: actions/cache@v4
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: gradle-${{ hashFiles('packages/mobile/android/**/*.gradle', 'packages/mobile/android/gradle/wrapper/gradle-wrapper.properties') }}
restore-keys: gradle-
- name: Build unsigned release APK
working-directory: packages/mobile/android
run: ./gradlew assembleRelease --no-daemon --stacktrace
# O nome do arquivo muda conforme a versão do AGP (app-release-unsigned.apk
# ou app-release.apk quando não há config de assinatura). Localiza em vez
# de supor, e falha alto se nada saiu — um artefato vazio só apareceria
# como erro lá na frente, na hora de assinar.
- name: Collect APK
run: |
set -euo pipefail
mkdir -p out
found=$(find packages/mobile/android/app/build/outputs/apk/release -name '*.apk' | head -1)
[ -n "$found" ] || { echo "::error::o build não produziu APK nenhum"; exit 1; }
cp "$found" out/resenhacord-unsigned.apk
ls -l out/
- name: Upload APK artifact
uses: actions/upload-artifact@v4
with:
name: resenhacord-apk-unsigned
path: out/resenhacord-unsigned.apk
retention-days: 14
if-no-files-found: error
+45 -7
View File
@@ -153,14 +153,38 @@ jobs:
# Remove só os anexos que esta plataforma vai repor, para os dois jobs # Remove só os anexos que esta plataforma vai repor, para os dois jobs
# não apagarem o trabalho um do outro. # não apagarem o trabalho um do outro.
#
# Padrão montado em bash puro: a versão anterior interpolava uma
# expressão do Actions dentro de um `case`, e quando ela não casou o
# laço passou em silêncio — a release ficou com dois latest.yml e o
# updater serviu o antigo, dizendo que a versão nova não existia.
R=$(api GET "/releases/$ID/assets") R=$(api GET "/releases/$ID/assets")
if [ "$(code "$R")" = "200" ]; then if [ "$(code "$R")" != "200" ]; then
body "$R" | jq -r '.[] | "\(.id) \(.name)"' | while read -r aid aname; do echo "::error::não foi possível listar os anexos — HTTP $(code "$R"): $(body "$R")"
case "$aname" in exit 1
${{ runner.os == 'Windows' && '*.exe|latest.yml' || '*.AppImage|*.deb|latest-linux.yml' }}) fi
echo "removendo anexo antigo: $aname"
api DELETE "/releases/$ID/assets/$aid" > /dev/null ;; # Filtro dentro do jq, comparando o nome direto. A versão anterior
esac # montava um regex e o casava contra "<id> <nome>", onde o `^` de
# `^latest\.yml$` ancorava no início da linha — depois do id — e nunca
# podia casar. O laço então só removia os .exe e deixava um latest.yml
# duplicado, que é o arquivo que decide se há atualização.
OLD=$(body "$R" | jq -r --arg os "${RUNNER_OS:-}" '
.[]
| select(
if $os == "Windows"
then (.name | endswith(".exe")) or .name == "latest.yml"
else (.name | endswith(".AppImage")) or (.name | endswith(".deb")) or .name == "latest-linux.yml"
end
)
| "\(.id) \(.name)"')
echo "anexos desta plataforma já na release: $(printf '%s' "$OLD" | grep -c . || true)"
if [ -n "$OLD" ]; then
printf '%s\n' "$OLD" | while read -r aid aname; do
[ -n "$aid" ] || continue
echo "removendo anexo antigo: $aname"
D=$(api DELETE "/releases/$ID/assets/$aid")
[ "$(code "$D")" = "204" ] || echo "::warning::falha ao remover $aname — HTTP $(code "$D")"
done done
fi fi
@@ -181,3 +205,17 @@ jobs:
done done
[ "$sent" -gt 0 ] || { echo "::error::o build não produziu instaladores"; exit 1; } [ "$sent" -gt 0 ] || { echo "::error::o build não produziu instaladores"; exit 1; }
echo "$sent arquivo(s) publicados" echo "$sent arquivo(s) publicados"
# O updater busca latest.yml pelo nome. Duas cópias com o mesmo nome
# fazem o Gitea servir a mais antiga, e a atualização deixa de ser
# oferecida — sem erro em lugar nenhum. Falha aqui em vez de publicar
# uma release que parece boa e não atualiza.
if [ "${RUNNER_OS:-}" = "Windows" ]; then
R=$(api GET "/releases/$ID/assets")
DUP=$(body "$R" | jq -r '[.[] | select(.name == "latest.yml")] | length')
if [ "$DUP" != "1" ]; then
echo "::error::a release tem $DUP cópias de latest.yml — o updater serviria a errada"
exit 1
fi
echo "latest.yml: 1 cópia, como esperado"
fi
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 92 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 200 KiB

After

Width:  |  Height:  |  Size: 25 KiB

+33 -26
View File
@@ -1,33 +1,40 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<svg id="Ebene_2" data-name="Ebene 2" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 147.46 146.59"> <!--
Resenhacord app icon: the mark on the app's own night-blue ground (#0b0b10,
the theme colour), inside the 22 %-radius rounded square the icon pipeline
masks to. A soft violet glow behind the mark keeps the badge from reading as
a flat black square at large sizes.
-->
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024">
<defs> <defs>
<style> <radialGradient id="glow" cx="512" cy="480" r="420" gradientUnits="userSpaceOnUse">
.cls-1 { <stop offset="0" stop-color="#7c6cf6" stop-opacity="0.28"/>
fill: #1d1d1b; <stop offset="1" stop-color="#7c6cf6" stop-opacity="0"/>
} </radialGradient>
<linearGradient id="back" x1="10" y1="12" x2="74" y2="74" gradientUnits="userSpaceOnUse">
.cls-2 { <stop offset="0" stop-color="#c4b5fd"/>
fill: url(#Unbenannter_Verlauf_515); <stop offset="1" stop-color="#7c6cf6"/>
}
.cls-3 {
fill: url(#Unbenannter_Verlauf_547);
}
</style>
<linearGradient id="Unbenannter_Verlauf_515" data-name="Unbenannter Verlauf 515" x1="62.13" y1="77.74" x2="118.16" y2="77.74" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#fff"/>
<stop offset=".95" stop-color="#9a86b8"/>
</linearGradient> </linearGradient>
<linearGradient id="Unbenannter_Verlauf_547" data-name="Unbenannter Verlauf 547" x1="9.21" y1="71.45" x2="135.41" y2="71.45" gradientUnits="userSpaceOnUse"> <linearGradient id="front" x1="42" y1="42" x2="94" y2="94" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#fff"/> <stop offset="0" stop-color="#fda4af"/>
<stop offset=".95" stop-color="#9a86b8"/> <stop offset="1" stop-color="#f472b6"/>
</linearGradient> </linearGradient>
<path id="frontBubble" d="M56 42 H80 A14 14 0 0 1 94 56 V68 A14 14 0 0 1 80 82 H76 L88 94 L68 82 H56 A14 14 0 0 1 42 68 V56 A14 14 0 0 1 56 42 Z"/>
<mask id="gap" maskUnits="userSpaceOnUse" x="0" y="0" width="100" height="100">
<rect width="100" height="100" fill="#fff"/>
<use href="#frontBubble" fill="#000" stroke="#000" stroke-width="7" stroke-linejoin="round"/>
</mask>
</defs> </defs>
<g id="Ebene_1-2" data-name="Ebene 1"> <rect width="1024" height="1024" rx="225" fill="#0b0b10"/>
<rect class="cls-1" x="0" y="0" width="147.46" height="146.59" rx="32.42" ry="32.42"/> <rect width="1024" height="1024" rx="225" fill="url(#glow)"/>
<g> <g transform="translate(192 192) scale(6.4)">
<path class="cls-2" d="M100.85,75.32c9.67-3.15,14.27-9.44,14.27-20.12,0-13.48-9.33-21.23-25.52-21.23h-16.13c-7.98,0-11.35,3.48-11.35,12.14v53.82h0c5.77,0,10.46-4.68,10.46-10.46v-7.44c-.06-.33-.1-.68-.09-1.05,0-3.35.07-6.69.12-10.04h-.03v-24.05c0-2.24,1.12-3.6,3.25-3.6h12.99c10.23,0,15.51,4.5,15.51,13.26s-5.62,14.38-15.29,14.38h-6.04c-.04,3.08-.1,6.14-.11,9.22h6.5c11.46,0,17.64,5.51,17.64,16.08s-8.01,16.22-19.36,16.22l-4.36.38h0l-10.57.41c-6.01,0-6.3,8.01-.3,8.25.34.01.68.02,1.03.02h17.37c17.32,0,27.31-8.99,27.31-24.28,0-12.14-5.84-19.56-17.3-21.92Z"/> <path mask="url(#gap)" fill="url(#back)"
<path class="cls-3" d="M102.43,119.43c-.64.25-1.29.48-1.93.7l-1.67.39c-.57.14-1.04.17-1.57.26-.51.09-1.02.16-1.51.21-.98.11-1.95.2-2.9.25-.96.07-1.88.12-2.82.16-3.72.14-7.36.14-10.98.07-1.81-.03-3.61-.1-5.41-.16l-2.67-.11c-.83-.03-1.76-.08-2.69-.13l-5.55-.32-.63-.04-.81-.09c-.54-.08-1.08-.16-1.62-.27-1.07-.22-2.13-.5-3.17-.88-2.07-.74-4.05-1.78-5.83-3.11-.9-.66-1.74-1.39-2.53-2.18l-1.91-1.98-7.37-7.7-7.37-7.7c-2.49-2.54-4.81-5.23-7.56-7.52-2.75-2.29-5.15-4.91-7.6-7.49-1.23-1.29-2.4-2.62-3.61-3.93l-.45-.49-.22-.25-.34-.38c-.59-.67-1.07-1.43-1.47-2.23-.77-1.61-1.13-3.41-.99-5.18.11-1.76.72-3.49,1.7-4.96.26-.35.51-.72.81-1.04l.45-.48.25-.25.96-.93,3.83-3.71,7.66-7.42,15.32-14.84,7.66-7.42.99-.95c.4-.38.8-.74,1.23-1.09.85-.69,1.73-1.33,2.67-1.87,1.87-1.11,3.91-1.92,6.02-2.42,1.06-.24,2.13-.4,3.2-.49.54-.04,1.08-.06,1.62-.06h.73s.67.02.67.02l5.33.11,10.66.21,21.32.42,10.72.23c4.1.16,8.16,1.58,11.38,4.07.83.6,1.56,1.31,2.29,2.01.67.76,1.36,1.51,1.93,2.35,1.16,1.65,2.12,3.46,2.7,5.4.59,1.92.97,3.93.94,5.93,0,1.87-.06,3.58-.09,5.38l-.85,42.64-.22,10.68c-.12,3.6-.47,7.31-1.88,10.77-.69,1.72-1.64,3.4-2.98,4.74-1.32,1.36-2.95,2.38-4.68,3.05-1.75.64-3.58.92-5.39,1.06-.9.02-1.82.04-2.71.02-.44-.01-.89,0-1.33-.06-.22-.02-.44-.06-.67-.1l-.33-.07c-.11-.03-.22.01-.33-.26.11-.27.22-.22.33-.26l.33-.07c.22-.04.44-.08.67-.1.44-.05.89-.04,1.33-.06.89-.02,1.75-.07,2.62-.12,1.72-.2,3.42-.54,5-1.18,1.55-.67,2.95-1.64,4.06-2.88,1.13-1.22,1.92-2.74,2.48-4.33,1.16-3.19,1.37-6.71,1.34-10.18l-.2-10.64-.85-42.65c-.04-1.76-.05-3.61-.12-5.28-.04-1.56-.4-3.08-.9-4.52-.5-1.46-1.29-2.79-2.21-4.01-.45-.62-1-1.16-1.52-1.71-.58-.5-1.14-1.02-1.78-1.43-2.48-1.76-5.49-2.67-8.48-2.67l-10.6.19-21.32.42-10.66.21-5.33.11h-.67c-.23.02-.45.02-.6.03-.35,0-.7.04-1.04.08-.69.09-1.38.22-2.05.4-1.34.38-2.61.95-3.77,1.7-.58.37-1.12.8-1.64,1.25-.26.23-.5.47-.74.71l-.9.94-7.36,7.72-14.72,15.43-7.36,7.72-3.68,3.86-.92.96-.22.23s0,.01,0,.01c-.01,0,.01.02,0,.01.02.02.02.02.05.04,0,.01.05,0,.04,0,.02-.02,0-.01.02-.03l.81.83c1.24,1.28,2.5,2.53,3.71,3.82,2.43,2.59,4.91,5.14,7.04,8.02,2.13,2.88,4.69,5.35,7.08,7.98l7.27,7.8,7.27,7.8,1.76,1.89c.46.48.95.92,1.48,1.32,1.06.8,2.27,1.43,3.54,1.88.64.23,1.3.4,1.97.54.33.07.67.12,1.01.17l.51.06c.33.03.46.03.7.04l5.09.27c.84.05,1.69.1,2.63.12l2.65.1c1.76.06,3.51.12,5.25.15,3.48.06,6.96.07,10.34-.17,1.68-.1,3.36-.28,4.9-.57.4-.06.77-.14,1.12-.23.35-.09.76-.16,1.04-.27l.9-.31c.18-.11.36-.22.54-.3l.65-.29c2.43-1.07,5.26.03,6.33,2.46,1.07,2.43-.03,5.26-2.46,6.33-.05.02-.12.05-.17.07Z"/> d="M26 12 H58 A16 16 0 0 1 74 28 V44 A16 16 0 0 1 58 60 H34 L16 74 L20 60 H26 A16 16 0 0 1 10 44 V28 A16 16 0 0 1 26 12 Z"/>
<use href="#frontBubble" fill="url(#front)"/>
<g fill="#fff">
<circle cx="58" cy="62" r="3.6"/>
<circle cx="68" cy="62" r="3.6"/>
<circle cx="78" cy="62" r="3.6"/>
</g> </g>
</g> </g>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

+17 -11
View File
@@ -1,14 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<svg id="Ebene_2" data-name="Ebene 2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 123.08 100.14"> <!-- Monochrome silhouette of mark.svg for macOS menu-bar templates (alpha only). -->
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<defs> <defs>
<style> <path id="frontBubble" d="M56 42 H80 A14 14 0 0 1 94 56 V68 A14 14 0 0 1 80 82 H76 L88 94 L68 82 H56 A14 14 0 0 1 42 68 V56 A14 14 0 0 1 56 42 Z"/>
.cls-1 { <mask id="gap">
fill: #1d1d1b; <rect width="100" height="100" fill="#fff"/>
} <use href="#frontBubble" fill="#000" stroke="#000" stroke-width="7" stroke-linejoin="round"/>
</style> </mask>
<mask id="dots">
<rect width="100" height="100" fill="#fff"/>
<circle cx="58" cy="62" r="3.6" fill="#000"/>
<circle cx="68" cy="62" r="3.6" fill="#000"/>
<circle cx="78" cy="62" r="3.6" fill="#000"/>
</mask>
</defs> </defs>
<g id="Ebene_1-2" data-name="Ebene 1"> <path mask="url(#gap)" fill="#1d1d1b"
<path class="cls-1" d="M91.64,53.93c9.67-3.15,14.27-9.44,14.27-20.12,0-13.48-9.33-21.23-25.52-21.23h-16.13c-7.98,0-11.35,3.48-11.35,12.14v53.82h0c5.77,0,10.46-4.68,10.46-10.46v-7.44c-.06-.33-.1-.68-.09-1.05,0-3.35.07-6.69.12-10.04h-.03v-24.05c0-2.24,1.12-3.6,3.25-3.6h12.99c10.23,0,15.51,4.5,15.51,13.26s-5.62,14.38-15.29,14.38h-6.04c-.04,3.08-.1,6.14-.11,9.22h6.5c11.46,0,17.64,5.51,17.64,16.08s-8.01,16.22-19.36,16.22l-4.36.38h0l-10.57.41c-6.01,0-6.3,8.01-.3,8.25.34.01.68.02,1.03.02h17.37c17.32,0,27.31-8.99,27.31-24.28,0-12.14-5.84-19.56-17.3-21.92Z"/> d="M26 12 H58 A16 16 0 0 1 74 28 V44 A16 16 0 0 1 58 60 H34 L16 74 L20 60 H26 A16 16 0 0 1 10 44 V28 A16 16 0 0 1 26 12 Z"/>
<path class="cls-1" d="M93.21,98.04c-1.58.67-3.48,1.14-5.16,1.36-7.86,1.04-15.77.76-23.61.52-3.06-.04-7.8-.42-10.92-.56-2.12-.13-4.23-.56-6.23-1.28-4.01-1.34-7.48-4.13-10.26-7.27-3.66-3.85-11.08-11.56-14.75-15.4-2.49-2.54-4.81-5.23-7.56-7.52-4.08-3.48-7.59-7.49-11.21-11.42-3.78-3.58-4.71-9.08-1.77-13.49.6-.98,1.66-1.88,2.47-2.69,6.89-6.67,19.79-19.17,26.81-25.96,0,0,7.66-7.42,7.66-7.42,1.47-1.45,3.08-2.88,4.89-3.91C46.83,1.04,50.61,0,54.4,0c.89-.03,5.73.13,6.73.12,0,0,10.66.21,10.66.21,7.02.15,25.23.49,32.04.65,8.24.22,15.88,5.97,18.3,13.83,1.26,3.64.92,7.57.85,11.31,0,0-.85,42.64-.85,42.64-.31,13.92,1.41,31-17.86,30.33-.78,0-1.57-.04-2.33-.23-.11-.03-.22.01-.33-.26.11-.27.22-.22.33-.26,3.35-.48,6.76-.1,9.95-1.53,6.81-2.78,8.04-10.78,7.88-17.39-.14-6.81-.88-45.63-1.05-53.3-.12-3.34.17-6.68-1.03-9.81-1.03-2.88-3-5.41-5.51-7.15-2.48-1.76-5.49-2.67-8.48-2.67,0,0-10.6.19-10.6.19-9.89.2-27.61.55-37.32.74-3.9-.11-7.81,1.33-10.51,4.18-.22.33-7.8,8.1-8.26,8.65-4.86,5.11-17.37,18.21-22.08,23.15-.73.81-4.11,4.27-4.82,5.05,0,0,0,.01,0,.01-.01,0,.01.02,0,.01.02.02.08.07.09.03.02-.02,0-.01.02-.03,0,0,.81.83.81.83,3.71,3.84,7.52,7.57,10.75,11.84,3.79,4.86,10.17,11.13,14.34,15.78,0,0,7.27,7.8,7.27,7.8,1.85,2.11,4.03,4.24,6.79,5.09,2.44.99,6.67.88,9.28,1.08,3.39.17,7.13.31,10.53.37,5.86.03,11.79.22,17.4-1.24,0,0,.9-.31.9-.31.35-.2.82-.44,1.2-.58,6.01-2.4,9.65,6.26,3.7,8.87h0Z"/> <use href="#frontBubble" fill="#1d1d1b" mask="url(#dots)"/>
</g> </svg>
</svg>

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

+27 -22
View File
@@ -1,28 +1,33 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<svg id="Ebene_2" data-name="Ebene 2" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 123.07 100.13"> <!--
Resenhacord mark: two speech bubbles in conversation, the front one talking.
Square viewBox so every raster target (tray, favicon, PWA) scales without
letterboxing. The gap between the bubbles is cut with a mask rather than
painted, so the mark stays legible on any background and the macOS template
(alpha-only) variant keeps the same silhouette.
-->
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<defs> <defs>
<style> <linearGradient id="back" x1="10" y1="12" x2="74" y2="74" gradientUnits="userSpaceOnUse">
.cls-1 { <stop offset="0" stop-color="#c4b5fd"/>
fill: url(#Unbenannter_Verlauf_515); <stop offset="1" stop-color="#7c6cf6"/>
}
.cls-2 {
fill: url(#Unbenannter_Verlauf_547);
}
</style>
<linearGradient id="Unbenannter_Verlauf_515" data-name="Unbenannter Verlauf 515" x1="52.91" y1="56.35" x2="108.94" y2="56.35" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#fff"/>
<stop offset=".95" stop-color="#9a86b8"/>
</linearGradient> </linearGradient>
<linearGradient id="Unbenannter_Verlauf_547" data-name="Unbenannter Verlauf 547" x1="0" y1="50.06" x2="126.19" y2="50.06" gradientUnits="userSpaceOnUse"> <linearGradient id="front" x1="42" y1="42" x2="94" y2="94" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#fff"/> <stop offset="0" stop-color="#fda4af"/>
<stop offset=".95" stop-color="#9a86b8"/> <stop offset="1" stop-color="#f472b6"/>
</linearGradient> </linearGradient>
<path id="frontBubble" d="M56 42 H80 A14 14 0 0 1 94 56 V68 A14 14 0 0 1 80 82 H76 L88 94 L68 82 H56 A14 14 0 0 1 42 68 V56 A14 14 0 0 1 56 42 Z"/>
<mask id="gap">
<rect width="100" height="100" fill="#fff"/>
<use href="#frontBubble" fill="#000" stroke="#000" stroke-width="7" stroke-linejoin="round"/>
</mask>
</defs> </defs>
<g id="Ebene_1-2" data-name="Ebene 1"> <path mask="url(#gap)" fill="url(#back)"
<g> d="M26 12 H58 A16 16 0 0 1 74 28 V44 A16 16 0 0 1 58 60 H34 L16 74 L20 60 H26 A16 16 0 0 1 10 44 V28 A16 16 0 0 1 26 12 Z"/>
<path class="cls-1" d="M91.64,53.93c9.67-3.15,14.27-9.44,14.27-20.12,0-13.48-9.33-21.23-25.52-21.23h-16.13c-7.98,0-11.35,3.48-11.35,12.14v53.82h0c5.77,0,10.46-4.68,10.46-10.46v-7.44c-.06-.33-.1-.68-.09-1.05,0-3.35.07-6.69.12-10.04h-.03v-24.05c0-2.24,1.12-3.6,3.25-3.6h12.99c10.23,0,15.51,4.5,15.51,13.26s-5.62,14.38-15.29,14.38h-6.04c-.04,3.08-.1,6.14-.11,9.22h6.5c11.46,0,17.64,5.51,17.64,16.08s-8.01,16.22-19.36,16.22l-4.36.38h0l-10.57.41c-6.01,0-6.3,8.01-.3,8.25.34.01.68.02,1.03.02h17.37c17.32,0,27.31-8.99,27.31-24.28,0-12.14-5.84-19.56-17.3-21.92Z"/> <use href="#frontBubble" fill="url(#front)"/>
<path class="cls-2" d="M93.21,98.04c-.64.25-1.29.48-1.93.7l-1.67.39c-.57.14-1.04.17-1.57.26-.51.09-1.02.16-1.51.21-.98.11-1.95.2-2.9.25-.96.07-1.88.12-2.82.16-3.72.14-7.36.14-10.98.07-1.81-.03-3.61-.1-5.41-.16l-2.67-.11c-.83-.03-1.76-.08-2.69-.13l-5.55-.32-.63-.04-.81-.09c-.54-.08-1.08-.16-1.62-.27-1.07-.22-2.13-.5-3.17-.88-2.07-.74-4.05-1.78-5.83-3.11-.9-.66-1.74-1.39-2.53-2.18l-1.91-1.98-7.37-7.7-7.37-7.7c-2.49-2.54-4.81-5.23-7.56-7.52-2.75-2.29-5.15-4.91-7.6-7.49-1.23-1.29-2.4-2.62-3.61-3.93l-.45-.49-.22-.25-.34-.38c-.59-.67-1.07-1.43-1.47-2.23-.77-1.61-1.13-3.41-.99-5.18.11-1.76.72-3.49,1.7-4.96.26-.35.51-.72.81-1.04l.45-.48.25-.25.96-.93,3.83-3.71,7.66-7.42,15.32-14.84,7.66-7.42.99-.95c.4-.38.8-.74,1.23-1.09.85-.69,1.73-1.33,2.67-1.87,1.87-1.11,3.91-1.92,6.02-2.42,1.06-.24,2.13-.4,3.2-.49.54-.04,1.08-.06,1.62-.06h.73s.67.02.67.02l5.33.11,10.66.21,21.32.42,10.72.23c4.1.16,8.16,1.58,11.38,4.07.83.6,1.56,1.31,2.29,2.01.67.76,1.36,1.51,1.93,2.35,1.16,1.65,2.12,3.46,2.7,5.4.59,1.92.97,3.93.94,5.93,0,1.87-.06,3.58-.09,5.38l-.85,42.64-.22,10.68c-.12,3.6-.47,7.31-1.88,10.77-.69,1.72-1.64,3.4-2.98,4.74-1.32,1.36-2.95,2.38-4.68,3.05-1.75.64-3.58.92-5.39,1.06-.9.02-1.82.04-2.71.02-.44-.01-.89,0-1.33-.06-.22-.02-.44-.06-.67-.1l-.33-.07c-.11-.03-.22.01-.33-.26.11-.27.22-.22.33-.26l.33-.07c.22-.04.44-.08.67-.1.44-.05.89-.04,1.33-.06.89-.02,1.75-.07,2.62-.12,1.72-.2,3.42-.54,5-1.18,1.55-.67,2.95-1.64,4.06-2.88,1.13-1.22,1.92-2.74,2.48-4.33,1.16-3.19,1.37-6.71,1.34-10.18l-.2-10.64-.85-42.65c-.04-1.76-.05-3.61-.12-5.28-.04-1.56-.4-3.08-.9-4.52-.5-1.46-1.29-2.79-2.21-4.01-.45-.62-1-1.16-1.52-1.71-.58-.5-1.14-1.02-1.78-1.43-2.48-1.76-5.49-2.67-8.48-2.67l-10.6.19-21.32.42-10.66.21-5.33.11h-.67c-.23.02-.45.02-.6.03-.35,0-.7.04-1.04.08-.69.09-1.38.22-2.05.4-1.34.38-2.61.95-3.77,1.7-.58.37-1.12.8-1.64,1.25-.26.23-.5.47-.74.71l-.9.94-7.36,7.72-14.72,15.43-7.36,7.72-3.68,3.86-.92.96-.22.23s0,.01,0,.01c-.01,0,.01.02,0,.01.02.02.02.02.05.04,0,.01.05,0,.04,0,.02-.02,0-.01.02-.03l.81.83c1.24,1.28,2.5,2.53,3.71,3.82,2.43,2.59,4.91,5.14,7.04,8.02,2.13,2.88,4.69,5.35,7.08,7.98l7.27,7.8,7.27,7.8,1.76,1.89c.46.48.95.92,1.48,1.32,1.06.8,2.27,1.43,3.54,1.88.64.23,1.3.4,1.97.54.33.07.67.12,1.01.17l.51.06c.33.03.46.03.7.04l5.09.27c.84.05,1.69.1,2.63.12l2.65.1c1.76.06,3.51.12,5.25.15,3.48.06,6.96.07,10.34-.17,1.68-.1,3.36-.28,4.9-.57.4-.06.77-.14,1.12-.23.35-.09.76-.16,1.04-.27l.9-.31c.18-.11.36-.22.54-.3l.65-.29c2.43-1.07,5.26.03,6.33,2.46,1.07,2.43-.03,5.26-2.46,6.33-.05.02-.12.05-.17.07Z"/> <g fill="#fff">
</g> <circle cx="58" cy="62" r="3.6"/>
<circle cx="68" cy="62" r="3.6"/>
<circle cx="78" cy="62" r="3.6"/>
</g> </g>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

+70 -5
View File
@@ -25,11 +25,25 @@ código e os commits seguem em inglês, como o resto do repositório.
| Soundboard no app desktop | `c899253e` | `window.prompt` não existe no Electron; campo inline no lugar | | Soundboard no app desktop | `c899253e` | `window.prompt` não existe no Electron; campo inline no lugar |
| Áudio do sistema sem eco | `d525bbb8` `5b848432` | Módulo nativo com isolamento por processo + ganho e preset de música | | Áudio do sistema sem eco | `d525bbb8` `5b848432` | Módulo nativo com isolamento por processo + ganho e preset de música |
| Instaladores publicados pelo Gitea | `ce6bba05` | GitHub compila, Gitea distribui; atualização automática ligada | | Instaladores publicados pelo Gitea | `ce6bba05` | GitHub compila, Gitea distribui; atualização automática ligada |
| Fixar mensagens no canal | `4ceb5cd6` | Estado na própria mensagem; painel no cabeçalho; limite de 50 |
| Busca: filtros embutidos e tradução | `50a8f12c` | Os filtros já existiam; faltava `de:fulano` e descoberta |
| Emojis e figurinhas do servidor | `7d003021` | `:nome:` no markdown sem plugin; figurinha como mensagem inteira |
| Fundação de i18n (en + pt-BR) | `688a1335` | `src/i18n/`; pt-BR é parcial e cai para inglês. Aba Idioma nas configurações | | Fundação de i18n (en + pt-BR) | `688a1335` | `src/i18n/`; pt-BR é parcial e cai para inglês. Aba Idioma nas configurações |
| Rebrand: **Resenhacord** + logo nova | (ver git log) | `productName` mudou, `appId` **não** — assim o NSIS troca a instalação antiga no lugar. `userData` migra `Backspace` → `Resenhacord` no primeiro boot (senão todo mundo cairia da sessão). Logo: duas bolhas de conversa, roxo → rosa; fonte em `assets/brand/*.svg`, PNGs e ícones gerados por `pnpm gen-icons` |
| Aviso "X fixou uma mensagem" | (ver git log) | Tabela `messages` ganhou `type` (migração 0018); a rota de pin posta uma mensagem de sistema. Dois links: pular pra mensagem e abrir o painel de fixadas |
| Menção `@everyone` | (ver git log) | Reusa o token `<@id>`; permissão `MENTION_EVERYONE` nova, concedida ao `@everyone` por padrão e retroativa via backfill no boot. Servidor neutraliza o token de quem não tem a permissão, no envio **e** na edição |
| Renomear canal (nome e tópico) | (ver git log) | Rota `PATCH /api/channels/:id` já existia; faltava interface. Normalização em `shared/constants` para servidor e web usarem a mesma regra |
| Volume da tela em 50% por padrão | (ver git log) | `DEFAULT_STREAM_VOLUME`; o publicador reforça o áudio do sistema, então o padrão anterior chegava alto demais |
| Sons 50% mais baixos | (ver git log) | Só `SFX_BASE_VOLUME` (0.8 → 0.4); os `.ogg` não foram regerados |
| Atualização do desktop verificada com mais frequência | (ver git log) | Era só na abertura e de 4 em 4 horas; agora a cada 30 min e ao focar a janela, com trava de 5 min |
| Preview de perfil nos participantes da call | (ver git log) | O popout já existia e era aberto de 11 lugares; **nenhum era de voz**. Ligado nas linhas da lista de voz e no nome dos tiles da grade | | Preview de perfil nos participantes da call | (ver git log) | O popout já existia e era aberto de 11 lugares; **nenhum era de voz**. Ligado nas linhas da lista de voz e no nome dos tiles da grade |
## Já existia no código (verificado, não construir de novo) ## Já existia no código (verificado, não construir de novo)
- **Busca com filtros** — servidor (`q`, `from`, `has`, `before`, `after`),
client de API e painel no `SearchPopover` já existiam completos. Só faltava
tradução, sintaxe embutida (`de:fulano`) e descoberta.
- **Animação de digitação** — `TypingIndicator.tsx`, três pontos `animate-bounce` - **Animação de digitação** — `TypingIndicator.tsx`, três pontos `animate-bounce`
escalonados em 0/150/300ms. escalonados em 0/150/300ms.
- **Sons de call/stream** — `SoundController.tsx`, montado no `AppLayout`: - **Sons de call/stream** — `SoundController.tsx`, montado no `AppLayout`:
@@ -51,12 +65,33 @@ Ordem sugerida: as pequenas primeiro, as grandes uma de cada vez.
| # | Feature | Tamanho | Observação técnica | | # | Feature | Tamanho | Observação técnica |
|---|---|---|---| |---|---|---|---|
## Identidade do perfil (entregue 2026-09-02, pedido: "completo e FODA")
Uma coluna JSON (`users.profile_style`, migração 0019) normalizada no servidor
por `shared/profile.ts`. Aba **Perfil** nas configurações com prévia ao vivo
(`ProfilePanel` renderiza o mesmo `ProfileCard` que o popout).
| Peça | Onde aparece | Detalhe |
|---|---|---|
| Estilo do nome — 10 fontes × 9 efeitos × 2 cores | popout, perfil completo | Fontes OFL em `public/fonts`; efeitos são CSS (`.name-fx-*`) |
| Decoração do avatar — 10 | popout, perfil, editor | SVG inline em `DecoratedAvatar`, sem imagens |
| Efeito do perfil — 9 | banner do popout e do perfil | Partículas CSS com posições semeadas (`ProfileEffectLayer`) |
| Nameplate — 10 + personalizado | lista de membros | `.nameplate-*` |
| Tema (2 cores) | fundo do cartão e banner sem imagem | |
| Pronomes, tag (emoji + 2–4 letras) | ao lado do nome; tag também na lista de membros | |
| Links (até 5) | popout e aba Sobre | Ícone detectado pelo host; só https |
| Quadro: jogo favorito, rotação (5), quero jogar (10), citação | aba Quadro do perfil; tira "Coleção" no popout | Busca na Steam via `/api/games/search` (proxy, cache 10 min); capas só de CDN da Steam |
| Emblemas derivados | popout e perfil | admin, dono, fundador (1º mês), veterano (1 ano), OG (6 meses), estiloso — nada é armazenado |
Fora do escopo desta entrega, candidatos a seguir: perfil por servidor
(apelido/avatar por servidor), emblemas concedidos pelo admin, molduras
animadas por upload, status com expiração.
## Pendente — ideias aprovadas ## Pendente — ideias aprovadas
| Feature | Tamanho | Observação técnica | | Feature | Tamanho | Observação técnica |
|---|---|---| |---|---|---|
| Watch party | Grande | O screen share do LiveKit já existe; falta sincronizar posição de reprodução entre participantes | | Watch party | Grande | O screen share do LiveKit já existe; falta sincronizar posição de reprodução entre participantes |
| Emojis e stickers do grupo | Média | `UPLOAD_DIR` e o pipeline de upload já existem; falta tabela por espaço e resolução no render de mensagem |
## Regra de idioma (a partir de 2026-08-31) ## Regra de idioma (a partir de 2026-08-31)
@@ -79,12 +114,44 @@ fixas por `t('chave')`. O que faltar cai para o inglês sozinho.
- Chat: composer e mensagens (placeholder, anexos, responder, reações) - Chat: composer e mensagens (placeholder, anexos, responder, reações)
- Barra lateral: servidores, canais e menus de contexto - Barra lateral: servidores, canais e menus de contexto
- Lista de membros (grupos por cargo, carregamento) - Lista de membros (grupos por cargo, carregamento)
- Busca (rótulos, filtros e sintaxe embutida)
- Registro de auditoria e Estatísticas (nasceram bilíngues) - Registro de auditoria e Estatísticas (nasceram bilíngues)
- Configurações do canal (visão geral, privacidade, exclusão e rótulos de permissão)
- Amigos: abas, listas, pedidos, aba de adicionar e cartões de descoberta
- Modal de convite (incluindo os motivos de falha por convite)
- Lista de membros da DM em grupo (remover e transferir propriedade)
- Configurações → Área de trabalho (início automático, atualizações, instância)
- Configurações → Voz: dispositivo de saída (completa a aba, junto com a entrada)
- Configurações → Atalhos (incluindo os nomes das ações e o aviso de conflito)
- Configurações → Conta (perfil, senha, conta desligada da origem, zona de perigo)
- Explorar servidores (cartões, entrar, pedir para entrar)
- Configurações → Voz: vídeo (prévia, permissões, escolha de câmera) — fecha a aba
- Criar canal, categoria e servidor
- DMs em grupo (nova DM, adicionar amigos, configurações do grupo, sair)
- Entrar num servidor (descoberta, código de convite, conectar e login federado)
- Excluir conta, transferir propriedade e a tela de erro geral
- Configurações do servidor → Visão geral (identidade, ícone, banner, zona de perigo)
- Configurações do servidor → Membros e banimentos
- Configurações do servidor → Cargos (incluindo a tabela de permissões)
- Configurações da categoria (renomear, privacidade, exceções, excluir)
- Configurações da instância → Geral (nome, descoberta, busca de GIFs)
- Mensagens de sistema do chat e a prévia delas na barra lateral
- Login, registro e a página de convite
- Voz: controles, informações de conexão, folha de entrada no celular, aba Voz
- Telas do celular (`Mobile*.tsx`)
- Navegação das configurações (servidor, usuário e instância) + Descoberta e
pedidos de entrada
Os rótulos de permissão viraram um conjunto único (`perm.*`), usado pelo painel
de cargos e pelos editores de exceção de canal e de categoria — antes havia
duas cópias da mesma lista.
### Fila sugerida de tradução ### Fila sugerida de tradução
Mensagens e composer · lista de membros · servidores e canais · amigos e DMs · Configurações → Instância: registro, federação, transmissão, armazenamento e
modais de convite · configurações restantes · telas de erro usuários (os cinco painéis grandes, só o dono vê) · `ConnectedInstances` ·
`UserProfileModal` · `MainContent` e `ChannelSidebar` · mensagens de erro
vindas de stores e hooks (`instanceStore`, `useLiveKit`, `transferStore`)
## Correções pendentes ## Correções pendentes
@@ -114,8 +181,6 @@ Escolhidas pelo dono, inspiradas no Discord. **Nenhuma iniciada.**
| Feature | Tamanho | Observação técnica | | Feature | Tamanho | Observação técnica |
|---|---|---| |---|---|---|
| **Fixar mensagens no canal** | Pequena | Tabela de fixadas por canal + rota de listagem; permissão natural é `MANAGE_MESSAGES` |
| **Busca com filtros** | Média | `SearchPopover` já existe; falta filtrar por autor, canal e presença de anexo — e índice no banco, senão fica lento quando o histórico crescer |
| **Resposta rápida de voz (soundboard por atalho)** | Pequena | O soundboard já está pronto; falta só marcar favoritos e ligar a atalhos de teclado, sem abrir painel. Cuidado: o limite de repetição do servidor continua valendo e é o que impede virar metralhadora | | **Resposta rápida de voz (soundboard por atalho)** | Pequena | O soundboard já está pronto; falta só marcar favoritos e ligar a atalhos de teclado, sem abrir painel. Cuidado: o limite de repetição do servidor continua valendo e é o que impede virar metralhadora |
| **Cargos com ícone e exibição separada** | Média | Cargos e cores já existem; falta ícone, o campo "exibir separadamente" e o agrupamento na lista de membros | | **Cargos com ícone e exibição separada** | Média | Cargos e cores já existem; falta ícone, o campo "exibir separadamente" e o agrupamento na lista de membros |
+26
View File
@@ -0,0 +1,26 @@
# Scripts de operação da instância
Rodam na VPS, em `/opt/ops`, onde os arquivos são links simbólicos para esta
pasta — o repositório é a única cópia de verdade. Antes disso eles existiam só
na VPS, sem versão nem backup: uma reinstalação do servidor levaria junto todo
o processo de publicação.
| script | o que faz |
| --- | --- |
| `call-check.sh` | Diz se tem alguém em call. Roda antes de reiniciar o servidor. |
| `release-desktop.sh` | Espelha o fork no GitHub e dispara a compilação dos instaladores de desktop. |
| `release-android.sh` | Compila o APK no GitHub, assina na VPS e publica no Gitea. |
## O que estes scripts leem e não está aqui
Nada de segredo entra no repositório. Os arquivos abaixo ficam só na VPS, com
permissão `600`:
- `/root/.github-token` — dispara e baixa os builds do GitHub.
- `/root/.gitea-token` — publica os anexos das releases.
- `/root/android-signing/` — a chave que assina o APK, mais a senha dela.
A chave do Android merece atenção especial: ela é o que prova ao Android que
uma atualização veio de nós. Se for perdida, ninguém consegue atualizar por
cima — todo mundo tem que desinstalar e instalar de novo, e não existe
recuperação. Mantenha uma cópia fora da VPS.
+52
View File
@@ -0,0 +1,52 @@
#!/usr/bin/env bash
# Reports how many people are currently in a call on this instance.
#
# Asks LiveKit directly (ListRooms over its twirp API) rather than inferring
# from the app: LiveKit is the authority on who is actually connected to media.
# Prints the participant count and exits 0 when the instance is idle, 1 when
# someone is in a call — so it can gate a deploy.
set -euo pipefail
ENV_FILE=${ENV_FILE:-/opt/backspace/.env}
# `|| true`: under `set -o pipefail` a grep with no match aborts the script
# before the check below, exiting 1 — which in this script's contract means
# "someone is in a call" rather than "I could not tell".
KEY=$(grep -E '^LIVEKIT_API_KEY=' "$ENV_FILE" 2>/dev/null | cut -d= -f2- || true)
SECRET=$(grep -E '^LIVEKIT_API_SECRET=' "$ENV_FILE" 2>/dev/null | cut -d= -f2- || true)
[ -n "$KEY" ] && [ -n "$SECRET" ] || { echo "LiveKit credentials not found in $ENV_FILE" >&2; exit 2; }
b64url() { openssl base64 -A | tr '+/' '-_' | tr -d '='; }
now=$(date +%s)
header=$(printf '%s' '{"alg":"HS256","typ":"JWT"}' | b64url)
payload=$(printf '{"iss":"%s","sub":"%s","nbf":%d,"exp":%d,"video":{"roomList":true}}' \
"$KEY" "$KEY" "$now" "$((now + 60))" | b64url)
sig=$(printf '%s.%s' "$header" "$payload" \
| openssl dgst -sha256 -hmac "$SECRET" -binary | b64url)
jwt="$header.$payload.$sig"
resp=$(curl -sS --max-time 10 -w '\n%{http_code}' -X POST http://127.0.0.1:7880/twirp/livekit.RoomService/ListRooms \
-H "Authorization: Bearer $jwt" -H 'Content-Type: application/json' -d '{}') || {
echo "LiveKit unreachable — refusing to report idle" >&2; exit 2; }
code=$(printf '%s' "$resp" | tail -n1)
resp=$(printf '%s' "$resp" | sed '$d')
# A failed query returns no participant counts, which would read as "idle" and
# green-light a deploy on top of a live call. Anything but a well-formed 200 is
# treated as unknown, not as empty.
if [ "$code" != "200" ] || ! printf '%s' "$resp" | grep -q '"rooms"'; then
echo "LiveKit answered HTTP $code without a room list — status unknown, not deploying" >&2
exit 2
fi
# numParticipants is omitted by protobuf JSON when zero, so absence means empty.
total=$(printf '%s' "$resp" | grep -oE '"numParticipants":[0-9]+' | cut -d: -f2 | paste -sd+ - | bc 2>/dev/null || true)
total=${total:-0}
if [ "$total" -gt 0 ]; then
echo "EM CALL: $total participante(s) — nao deployar"
exit 1
fi
echo "OCIOSO: ninguem em call"
exit 0
+120
View File
@@ -0,0 +1,120 @@
#!/usr/bin/env bash
# Baixa o APK compilado no GitHub, assina aqui e publica no Gitea.
#
# A chave de assinatura fica nesta VPS e nao sobe para o GitHub. Ela e o que
# prova que uma atualizacao veio de nos: um APK assinado com outra chave nao
# instala por cima do instalado, o Android recusa. Por isso o runner entrega o
# APK sem assinatura e a assinatura acontece aqui.
#
# Uso: release-android.sh [run_id]
# sem run_id, dispara um build novo e espera terminar.
set -euo pipefail
REPO=${REPO:-/opt/backspace}
GH_REPO=${GH_REPO:-syncwrld/resenhacord}
TOOLS=/opt/ops/android-tools/build-tools
KEYSTORE=/root/android-signing/resenhacord.keystore
PASSFILE=/root/android-signing/password.txt
OUT=/opt/ops/android-out
GITEA_API=https://git.resenha.website/api/v1/repos/devsyncwrld/backspace
TOKEN=$(cat /root/.github-token)
gh() { curl -s -H "Authorization: Bearer $TOKEN" -H "Accept: application/vnd.github+json" "$@"; }
# apksigner e um script que chama java, e esta VPS nao tem JDK instalado (nem
# deve ter: ela roda a producao, nao compila nada). Roda num container, com as
# ferramentas, a chave e a pasta de saida montadas. zipalign e binario nativo e
# roda direto no host.
apksigner() {
docker run --rm \
-v "$TOOLS":/tools:ro \
-v /root/android-signing:/ks:ro \
-v "$OUT":/out \
eclipse-temurin:21-jdk /tools/apksigner "$@"
}
for f in "$TOOLS/apksigner" "$TOOLS/zipalign" "$KEYSTORE" "$PASSFILE"; do
[ -e "$f" ] || { echo "faltando: $f" >&2; exit 1; }
done
RUN_ID=${1:-}
if [ -z "$RUN_ID" ]; then
echo "==> Disparando o build no GitHub"
gh -o /dev/null -X POST \
"https://api.github.com/repos/$GH_REPO/actions/workflows/android.yml/dispatches" \
-d '{"ref":"main"}'
sleep 10
RUN_ID=$(gh "https://api.github.com/repos/$GH_REPO/actions/workflows/android.yml/runs?per_page=1" \
| python3 -c "import sys,json;print(json.load(sys.stdin)['workflow_runs'][0]['id'])")
echo " run=$RUN_ID"
echo "==> Esperando (uns 4 min)"
for _ in $(seq 1 90); do
read -r status conclusion < <(gh "https://api.github.com/repos/$GH_REPO/actions/runs/$RUN_ID" \
| python3 -c "import sys,json;r=json.load(sys.stdin);print(r['status'],r['conclusion'])")
[ "$status" = "completed" ] && break
sleep 20
done
[ "${conclusion:-}" = "success" ] || { echo "build falhou: ${conclusion:-timeout}" >&2; exit 1; }
fi
echo "==> Baixando o APK sem assinatura"
mkdir -p "$OUT"
URL=$(gh "https://api.github.com/repos/$GH_REPO/actions/runs/$RUN_ID/artifacts" \
| python3 -c "import sys,json;a=json.load(sys.stdin)['artifacts'];print(a[0]['archive_download_url'] if a else '')")
[ -n "$URL" ] || { echo "o run $RUN_ID nao tem artefato" >&2; exit 1; }
curl -sL -H "Authorization: Bearer $TOKEN" "$URL" -o "$OUT/apk.zip"
rm -f "$OUT/resenhacord-unsigned.apk"
unzip -o -q "$OUT/apk.zip" -d "$OUT"
VERSION=$(python3 -c "import json;print(json.load(open('$REPO/package.json'))['version'])")
SIGNED="$OUT/Resenhacord-$VERSION.apk"
echo "==> Alinhando e assinando $VERSION"
# --key-pass omitido de proposito: sem ele o apksigner reusa a senha do
# keystore, que e a mesma. Passar o mesmo arquivo nas duas opcoes falha — ele le
# os arquivos de senha em sequencia e bate no fim do arquivo na segunda leitura.
# zipalign antes do apksigner, nao depois: o alinhamento move bytes dentro do
# zip e invalidaria a assinatura se viesse por ultimo.
"$TOOLS/zipalign" -p -f 4 "$OUT/resenhacord-unsigned.apk" "$OUT/aligned.apk"
apksigner sign \
--ks /ks/resenhacord.keystore --ks-pass file:/ks/password.txt \
--ks-key-alias resenhacord \
--out "/out/$(basename "$SIGNED")" /out/aligned.apk
rm -f "$OUT/aligned.apk" "$OUT/apk.zip"
echo "==> Conferindo a assinatura"
apksigner verify --print-certs "/out/$(basename "$SIGNED")" | head -5
if [ "${1:-}" = "--no-publish" ] || [ "${PUBLISH:-1}" = "0" ]; then
echo "APK pronto: $SIGNED"
exit 0
fi
echo "==> Publicando no Gitea"
GITEA_TOKEN=$(cat /root/.gitea-token 2>/dev/null || true)
if [ -z "$GITEA_TOKEN" ]; then
echo "sem /root/.gitea-token — APK pronto localmente em $SIGNED"
exit 0
fi
api() { curl -s -w '\n%{http_code}' -X "$1" -H "Authorization: token $GITEA_TOKEN" "$GITEA_API$2" "${@:3}"; }
body() { sed '$d' <<<"$1"; }
code() { tail -n1 <<<"$1"; }
R=$(api GET "/releases/tags/latest")
ID=$(body "$R" | python3 -c "import sys,json;print(json.load(sys.stdin).get('id',''))" 2>/dev/null || echo "")
[ -n "$ID" ] || { echo "release 'latest' nao encontrada no Gitea" >&2; exit 1; }
# Remove o APK anterior: dois anexos com o mesmo nome fazem o Gitea servir o
# mais antigo, e quem baixasse pegaria a versao velha sem erro nenhum.
api GET "/releases/$ID/assets" | sed '$d' \
| python3 -c "import sys,json;[print(a['id']) for a in json.load(sys.stdin) if a['name'].endswith('.apk')]" \
| while read -r aid; do
[ -n "$aid" ] && api DELETE "/releases/$ID/assets/$aid" >/dev/null && echo " removido anexo $aid"
done
NAME=$(basename "$SIGNED")
R=$(api POST "/releases/$ID/assets?name=$NAME" -F "attachment=@$SIGNED")
[ "$(code "$R")" = "201" ] || { echo "falha no upload — HTTP $(code "$R"): $(body "$R")" >&2; exit 1; }
echo "publicado: $NAME"
echo "https://git.resenha.website/devsyncwrld/backspace/releases/download/latest/$NAME"
+39
View File
@@ -0,0 +1,39 @@
#!/usr/bin/env bash
# Espelha o fork no GitHub e dispara a compilacao dos instaladores.
#
# O GitHub e usado apenas como maquina de build: ele fornece runners Windows
# prontos, e o modulo nativo do audio (WASAPI) so compila la. O Gitea continua
# sendo o repositorio principal.
set -euo pipefail
REPO=${REPO:-/opt/backspace}
TAG=${1:-}
cd "$REPO"
if [ -z "$TAG" ]; then
echo "uso: $0 vX.Y.Z" >&2
exit 2
fi
# `ssh -T git@github.com` always exits 1 (GitHub refuses shell access), and
# under `pipefail` that status wins over grep's — so piping straight into the
# `if` reported failure even on a working key. Capture, then test.
SSH_PROBE="$(ssh -o BatchMode=yes -o StrictHostKeyChecking=accept-new -T git@github.com 2>&1 || true)"
if ! printf '%s' "$SSH_PROBE" | grep -q "successfully authenticated"; then
echo "GitHub ainda nao aceita a chave desta VPS." >&2
echo "Adicione esta chave em https://github.com/settings/keys :" >&2
cat ~/.ssh/id_ed25519.pub >&2
exit 1
fi
echo "==> Espelhando main no GitHub"
git push github main
echo "==> Marcando $TAG (e a tag que dispara o workflow release.yml)"
git tag -f "$TAG"
git push -f github "$TAG"
echo
echo "Compilacao disparada. Acompanhe em:"
echo " https://github.com/syncwrld/resenhacord/actions"
echo "Os instaladores aparecem em Releases quando terminar (~15 min)."
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "backspace", "name": "backspace",
"version": "1.1.0", "version": "2.1.0",
"private": true, "private": true,
"description": "Open, self-hosted communication platform \u2014 text, voice, video, and federation", "description": "Open, self-hosted communication platform \u2014 text, voice, video, and federation",
"license": "AGPL-3.0-only", "license": "AGPL-3.0-only",
Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 364 KiB

After

Width:  |  Height:  |  Size: 364 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 368 KiB

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 955 B

After

Width:  |  Height:  |  Size: 712 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 368 KiB

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.8 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

+14 -4
View File
@@ -1,5 +1,5 @@
appId: com.backspace.desktop appId: com.backspace.desktop
productName: Backspace productName: Resenhacord
artifactName: "${productName}-${version}-${arch}.${ext}" artifactName: "${productName}-${version}-${arch}.${ext}"
directories: directories:
output: dist-electron output: dist-electron
@@ -36,7 +36,7 @@ publish:
- provider: generic - provider: generic
url: https://git.resenha.website/devsyncwrld/backspace/releases/download/latest/ url: https://git.resenha.website/devsyncwrld/backspace/releases/download/latest/
protocols: protocols:
- name: Backspace - name: Resenhacord
schemes: schemes:
- backspace - backspace
mac: mac:
@@ -58,5 +58,15 @@ linux:
- deb - deb
category: Network category: Network
nsis: nsis:
oneClick: false # One-click, per-user install (%LOCALAPPDATA%), the way Discord ships.
allowToChangeInstallationDirectory: true #
# This is what makes an update silent: a per-user install needs no elevation,
# so electron-updater can run the installer with /S and no UAC prompt breaks
# the flow. The assisted installer we had before (oneClick: false) showed its
# wizard on every update and offered an all-users install, which would then
# demand elevation on each subsequent update.
#
# The trade is that a fresh install no longer asks where to put the app.
oneClick: true
perMachine: false
allowToChangeInstallationDirectory: false
+2 -2
View File
@@ -1,9 +1,9 @@
{ {
"name": "@backspace/desktop", "name": "@backspace/desktop",
"version": "1.1.0", "version": "2.1.0",
"private": true, "private": true,
"license": "AGPL-3.0-only", "license": "AGPL-3.0-only",
"description": "Backspace", "description": "Resenhacord",
"author": { "author": {
"name": "Jannis Braun", "name": "Jannis Braun",
"email": "backspace@backspace.chat" "email": "backspace@backspace.chat"
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Backspace — Connect</title> <title>Resenhacord — Conectar</title>
<style> <style>
@import url('https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600;700&display=swap'); @import url('https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600;700&display=swap');
@@ -270,7 +270,7 @@
<div class="container"> <div class="container">
<div class="card"> <div class="card">
<div class="header"> <div class="header">
<h1>Welcome to Backspace</h1> <h1>Bem-vindo ao Resenhacord</h1>
<p>Connect to your instance</p> <p>Connect to your instance</p>
</div> </div>
@@ -360,7 +360,7 @@
// Update header copy to reflect the "switch" intent rather than the // Update header copy to reflect the "switch" intent rather than the
// first-run "welcome" framing. // first-run "welcome" framing.
document.querySelector('.header h1').textContent = 'Switch instance'; document.querySelector('.header h1').textContent = 'Switch instance';
document.querySelector('.header p').textContent = 'Connect to a different Backspace instance, or cancel to stay.'; document.querySelector('.header p').textContent = 'Conecte-se a outra instância, ou cancele para ficar nesta.';
} }
} }
init(); init();
+4 -4
View File
@@ -2,7 +2,7 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<title>Backspace — Recovery</title> <title>Resenhacord — Recuperação</title>
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<style> <style>
* { box-sizing: border-box; } * { box-sizing: border-box; }
@@ -128,7 +128,7 @@
<div class="content"> <div class="content">
<div class="container"> <div class="container">
<div class="card"> <div class="card">
<h1>Backspace couldn't load.</h1> <h1>O Resenhacord não conseguiu abrir.</h1>
<p class="hint" id="hint">Loading…</p> <p class="hint" id="hint">Loading…</p>
<div class="actions"> <div class="actions">
@@ -147,7 +147,7 @@
<pre id="diagnostic">loading…</pre> <pre id="diagnostic">loading…</pre>
</details> </details>
<button class="action subtle" id="btn-quit">Quit Backspace</button> <button class="action subtle" id="btn-quit">Fechar o Resenhacord</button>
</div> </div>
</div> </div>
</div> </div>
@@ -270,7 +270,7 @@
async function init() { async function init() {
if (!window.backspace) { if (!window.backspace) {
$('hint').textContent = 'Recovery bridge unavailable. Please quit and reinstall Backspace.'; $('hint').textContent = 'A ponte de recuperação não respondeu. Feche e reinstale o Resenhacord.';
return; return;
} }
try { try {
Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 983 B

After

Width:  |  Height:  |  Size: 746 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 703 B

After

Width:  |  Height:  |  Size: 474 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 836 B

+91 -33
View File
@@ -36,28 +36,36 @@ import {
buildTrayMenuTemplate, buildTrayMenuTemplate,
buildAppMenuTemplate, buildAppMenuTemplate,
type RecoveryState, type RecoveryState,
RELEASES_URL,
} from './recovery'; } from './recovery';
import { migrateUserData } from './userDataMigration'; import { migrateUserData } from './userDataMigration';
// Override Electron's package.json-derived app name so userData lives at // Override Electron's package.json-derived app name so userData lives at
// "<appData>/Backspace" instead of leaking the monorepo's "@backspace/desktop" // "<appData>/Resenhacord" instead of leaking the monorepo's "@backspace/desktop"
// package name. Must run before any app.getPath('userData') consumer. // package name. Must run before any app.getPath('userData') consumer.
app.setName('Backspace'); app.setName('Resenhacord');
// One-time migration from the historical scoped path. After the move the old // One-time migrations, oldest first, so an install that skipped a release
// folder is gone, so subsequent launches hit the old-missing no-op branch. // still walks the whole chain: "@backspace/desktop" → "Backspace" →
// "Resenhacord". Each step is a no-op once its source folder is gone, and a
// step whose target already has data leaves it alone (`new-populated`), so a
// user who already ran the renamed build is never overwritten by stale files.
// Without the second step the rename would have logged everyone out: the
// instance URL and session live under userData.
{ {
const appDataDir = app.getPath('appData'); const appDataDir = app.getPath('appData');
const oldParent = path.join(appDataDir, '@backspace'); const legacyParent = path.join(appDataDir, '@backspace');
const result = migrateUserData({ const steps: { oldDir: string; newDir: string; oldParent: string }[] = [
oldDir: path.join(oldParent, 'desktop'), { oldDir: path.join(legacyParent, 'desktop'), newDir: path.join(appDataDir, 'Backspace'), oldParent: legacyParent },
newDir: path.join(appDataDir, 'Backspace'), { oldDir: path.join(appDataDir, 'Backspace'), newDir: path.join(appDataDir, 'Resenhacord'), oldParent: appDataDir },
oldParent, ];
}); for (const step of steps) {
if (result.kind === 'migrated') { const result = migrateUserData(step);
console.log(`[userData] migrated ${result.from} → ${result.to}`); if (result.kind === 'migrated') {
} else if (result.kind === 'failed') { console.log(`[userData] migrated ${result.from} → ${result.to}`);
console.error('[userData] migration failed:', result.error); } else if (result.kind === 'failed') {
console.error('[userData] migration failed:', result.error);
}
} }
} }
@@ -241,7 +249,7 @@ function applyLoginItemSettings(openAtLogin: boolean, startMinimized: boolean):
enabled: openAtLogin, enabled: openAtLogin,
path: process.execPath, path: process.execPath,
args: startMinimized ? ['--hidden'] : [], args: startMinimized ? ['--hidden'] : [],
name: 'Backspace', name: 'Resenhacord',
}); });
} else { } else {
// Linux: setLoginItemSettings creates ~/.config/autostart/<name>.desktop. // Linux: setLoginItemSettings creates ~/.config/autostart/<name>.desktop.
@@ -437,7 +445,7 @@ function createWindow(): void {
: {}), : {}),
minWidth: 940, minWidth: 940,
minHeight: 500, minHeight: 500,
title: 'Backspace', title: 'Resenhacord',
icon: path.join(__dirname, '..', 'build', 'icon.png'), icon: path.join(__dirname, '..', 'build', 'icon.png'),
titleBarStyle: process.platform === 'darwin' ? 'hiddenInset' : 'hidden', titleBarStyle: process.platform === 'darwin' ? 'hiddenInset' : 'hidden',
...(process.platform !== 'darwin' ? { ...(process.platform !== 'darwin' ? {
@@ -570,7 +578,7 @@ function createTray(): void {
const icon = loadTrayIcon(); const icon = loadTrayIcon();
tray = new Tray(icon); tray = new Tray(icon);
tray.setToolTip('Backspace'); tray.setToolTip('Resenhacord');
// Context menu is set by the recoveryStore subscriber in app.whenReady, // Context menu is set by the recoveryStore subscriber in app.whenReady,
// which keeps the menu in sync with update/recovery state. // which keeps the menu in sync with update/recovery state.
@@ -588,7 +596,9 @@ function createTray(): void {
function showNotification(title: string, body: string, onClick?: () => void): void { function showNotification(title: string, body: string, onClick?: () => void): void {
if (!Notification.isSupported()) return; if (!Notification.isSupported()) return;
const notification = new Notification({ title, body, silent: false }); // silent: o som do sistema é o que mais incomoda numa notificação de chat, e
// o app toca o seu próprio efeito — que combina com os demais sons dele.
const notification = new Notification({ title, body, silent: true });
notification.on('click', onClick ?? (() => { notification.on('click', onClick ?? (() => {
mainWindow?.show(); mainWindow?.show();
mainWindow?.focus(); mainWindow?.focus();
@@ -672,12 +682,25 @@ function registerIpcHandlers(): void {
ipcMain.on('install-update', () => { ipcMain.on('install-update', () => {
try { try {
const { autoUpdater } = require('electron-updater'); const { autoUpdater } = require('electron-updater');
autoUpdater.quitAndInstall(); quitAndInstallSilently(autoUpdater);
} catch { } catch {
// Auto-updater not available // Auto-updater not available
} }
}); });
// The `update-downloaded` push is a one-shot: it reaches whatever page is
// loaded at that instant, and nothing else. It is lost when the window is in
// the tray (destroyed), when the check lands before the web app has mounted
// its listener, and on every reload. Since `downloaded` also latches off
// further polls, the banner could then never appear at all — which is why
// pressing "check for updates" by hand looked like the only thing that
// worked. The renderer now asks for the current state on mount instead of
// relying solely on catching the event live.
ipcMain.handle('get-update-status', () => {
const s = recoveryStore.get();
return { state: s.updateState, version: s.updateVersion };
});
ipcMain.on('check-for-updates', () => { ipcMain.on('check-for-updates', () => {
try { try {
const { autoUpdater } = require('electron-updater'); const { autoUpdater } = require('electron-updater');
@@ -708,7 +731,7 @@ function registerIpcHandlers(): void {
// executableWillLaunchAtLogin to honour Task Manager's StartupApproved state. // executableWillLaunchAtLogin to honour Task Manager's StartupApproved state.
const osState = app.getLoginItemSettings({ path: process.execPath }); const osState = app.getLoginItemSettings({ path: process.execPath });
const ownEntry = osState.launchItems?.find( const ownEntry = osState.launchItems?.find(
(item) => item.name === 'Backspace' || item.path?.toLowerCase() === process.execPath.toLowerCase(), (item) => item.name === 'Resenhacord' || item.name === 'Backspace' || item.path?.toLowerCase() === process.execPath.toLowerCase(),
); );
// When the Run entry exists, its args are the source of truth for startMinimized. // When the Run entry exists, its args are the source of truth for startMinimized.
// When the entry is absent (autostart is off), there is no OS state to read, so we // When the entry is absent (autostart is off), there is no OS state to read, so we
@@ -743,7 +766,7 @@ function registerIpcHandlers(): void {
if (process.platform === 'win32') { if (process.platform === 'win32') {
const osState = app.getLoginItemSettings({ path: process.execPath }); const osState = app.getLoginItemSettings({ path: process.execPath });
const ownEntry = osState.launchItems?.find( const ownEntry = osState.launchItems?.find(
(item) => item.name === 'Backspace' || item.path?.toLowerCase() === process.execPath.toLowerCase(), (item) => item.name === 'Resenhacord' || item.name === 'Backspace' || item.path?.toLowerCase() === process.execPath.toLowerCase(),
); );
currentOpenAtLogin = osState.executableWillLaunchAtLogin ?? false; currentOpenAtLogin = osState.executableWillLaunchAtLogin ?? false;
// See `get-auto-launch-settings`: when the entry is absent, fall back to disk // See `get-auto-launch-settings`: when the entry is absent, fall back to disk
@@ -802,6 +825,22 @@ function registerIpcHandlers(): void {
// ─── Auto-Update ──────────────────────────────────────────────────────────── // ─── Auto-Update ────────────────────────────────────────────────────────────
/**
* Installs a staged update with no installer UI and relaunches the app.
*
* `quitAndInstall()` defaults to (isSilent=false, isForceRunAfter=false): on
* Windows that runs the NSIS installer with its full wizard, so "restart to
* update" meant clicking through a setup program. Silent mode passes /S and
* reuses the recorded install location; isForceRunAfter brings the app back up
* afterwards, which the wizard would otherwise leave to a checkbox.
*
* Both flags are inert on macOS and Linux, where the update is applied by
* swapping files — no installer runs either way.
*/
function quitAndInstallSilently(au: { quitAndInstall: (isSilent?: boolean, isForceRunAfter?: boolean) => void }): void {
au.quitAndInstall(true, true);
}
function initAutoUpdater(): void { function initAutoUpdater(): void {
try { try {
const { autoUpdater } = require('electron-updater'); const { autoUpdater } = require('electron-updater');
@@ -812,7 +851,14 @@ function initAutoUpdater(): void {
let updateConfirmed = false; let updateConfirmed = false;
// Cadence state. `lastCheckAt` is stamped from the event rather than from
// the callers, so a manual "check for updates" from the settings panel
// counts too and a focus arriving right after it doesn't check twice.
let lastCheckAt = 0;
let downloaded = false;
autoUpdater.on('checking-for-update', () => { autoUpdater.on('checking-for-update', () => {
lastCheckAt = Date.now();
recoveryStore.update({ updateState: 'checking', lastCheckResult: null }); recoveryStore.update({ updateState: 'checking', lastCheckResult: null });
updateConfirmed = false; updateConfirmed = false;
}); });
@@ -834,6 +880,7 @@ function initAutoUpdater(): void {
}); });
autoUpdater.on('update-downloaded', (info: { version: string }) => { autoUpdater.on('update-downloaded', (info: { version: string }) => {
downloaded = true;
const version = info.version.slice(0, 32); const version = info.version.slice(0, 32);
recoveryStore.update({ updateState: 'downloaded', updateVersion: version }); recoveryStore.update({ updateState: 'downloaded', updateVersion: version });
mainWindow?.webContents.send('update-downloaded', { version }); mainWindow?.webContents.send('update-downloaded', { version });
@@ -843,9 +890,9 @@ function initAutoUpdater(): void {
// (recovery mode) is visible — no need to also fire a native toast. // (recovery mode) is visible — no need to also fire a native toast.
if (!mainWindow?.isFocused()) { if (!mainWindow?.isFocused()) {
showNotification( showNotification(
'Backspace update ready', 'Resenhacord update ready',
`Click to restart and install version ${version}.`, `Click to restart and install version ${version}.`,
() => autoUpdater.quitAndInstall(), () => quitAndInstallSilently(autoUpdater),
); );
} }
}); });
@@ -868,22 +915,33 @@ function initAutoUpdater(): void {
if (updateConfirmed) { if (updateConfirmed) {
mainWindow?.webContents.send('update-error', { mainWindow?.webContents.send('update-error', {
message, message,
releaseUrl: 'https://github.com/TheZwiss/backspace/releases/latest', releaseUrl: RELEASES_URL,
}); });
} }
}); });
// Initial check with 10s delay (existing behavior) // A release published while the app sits open used to go unnoticed until
setTimeout(() => { // the next poll — the startup check had long since run, and four hours is
updateConfirmed = false; // a long time to look at a stale version. Poll more often, and also check
autoUpdater.checkForUpdates().catch(() => {}); // when the user comes back to the window, which is exactly when they would
}, 10_000); // notice an update anyway.
const POLL_INTERVAL_MS = 30 * 60 * 1000;
const MIN_GAP_MS = 5 * 60 * 1000; // keeps alt-tabbing off the feed
// Periodic check every 4 hours (existing behavior) const maybeCheck = (): void => {
setInterval(() => { if (downloaded) return; // already staged; nothing left to look for
if (Date.now() - lastCheckAt < MIN_GAP_MS) return;
updateConfirmed = false; updateConfirmed = false;
autoUpdater.checkForUpdates().catch(() => {}); autoUpdater.checkForUpdates().catch(() => {});
}, 4 * 60 * 60 * 1000); };
setTimeout(maybeCheck, 10_000);
setInterval(maybeCheck, POLL_INTERVAL_MS);
// App-level rather than mainWindow-level: the window is destroyed and
// recreated when closed to tray and reopened, and a listener bound to the
// old instance would go quiet after the first close.
app.on('browser-window-focus', maybeCheck);
} catch { } catch {
// Auto-updater unavailable — recovery surface still functions, just // Auto-updater unavailable — recovery surface still functions, just
// without update affordances. autoUpdaterRef stays null; recovery action // without update affordances. autoUpdaterRef stays null; recovery action
@@ -1083,7 +1141,7 @@ if (!gotTheLock) {
// AGPL-3.0 § 13: native About panel advertises the version + source repo. // AGPL-3.0 § 13: native About panel advertises the version + source repo.
app.setAboutPanelOptions({ app.setAboutPanelOptions({
applicationName: 'Backspace', applicationName: 'Resenhacord',
applicationVersion: app.getVersion(), applicationVersion: app.getVersion(),
copyright: `AGPL-3.0-only · Source: ${UPSTREAM_SOURCE_URL}`, copyright: `AGPL-3.0-only · Source: ${UPSTREAM_SOURCE_URL}`,
website: UPSTREAM_SOURCE_URL, website: UPSTREAM_SOURCE_URL,
+1
View File
@@ -39,6 +39,7 @@ contextBridge.exposeInMainWorld('backspace', {
checkForUpdates: () => { checkForUpdates: () => {
ipcRenderer.send('check-for-updates'); ipcRenderer.send('check-for-updates');
}, },
getUpdateStatus: () => ipcRenderer.invoke('get-update-status'),
getVersion: () => ipcRenderer.invoke('get-app-version'), getVersion: () => ipcRenderer.invoke('get-app-version'),
// Window focus // Window focus
+1 -1
View File
@@ -166,7 +166,7 @@ describe('buildTrayMenuTemplate', () => {
it('includes Show/Hide/Change Instance/Quit base items', () => { it('includes Show/Hide/Change Instance/Quit base items', () => {
const items = buildTrayMenuTemplate(defaultState()); const items = buildTrayMenuTemplate(defaultState());
const labels = items.map((i) => i.label); const labels = items.map((i) => i.label);
expect(labels).toContain('Show Backspace'); expect(labels).toContain('Mostrar o Resenhacord');
expect(labels).toContain('Hide'); expect(labels).toContain('Hide');
expect(labels).toContain('Change Instance'); expect(labels).toContain('Change Instance');
expect(labels).toContain('Quit'); expect(labels).toContain('Quit');
+13 -3
View File
@@ -17,6 +17,15 @@ export type UpdateState =
| 'downloaded' | 'downloaded'
| 'error'; | 'error';
/**
* Where a manual download lands when auto-update fails.
*
* The upstream GitHub repository this used to point at is private for this
* fork, so the link 404'd for everyone who followed it. Releases are on the
* fork's own Gitea, which is also where the updater feed lives.
*/
export const RELEASES_URL = 'https://git.resenha.website/devsyncwrld/backspace/releases';
export interface RecoveryState { export interface RecoveryState {
mode: 'normal' | 'recovery'; mode: 'normal' | 'recovery';
reason: { code: RecoveryReasonCode; detail: string } | null; reason: { code: RecoveryReasonCode; detail: string } | null;
@@ -122,7 +131,7 @@ export function buildTrayMenuTemplate(
actions?: Partial<MenuActions>, actions?: Partial<MenuActions>,
): MenuItemConstructorOptions[] { ): MenuItemConstructorOptions[] {
const items: MenuItemConstructorOptions[] = [ const items: MenuItemConstructorOptions[] = [
{ label: 'Show Backspace', click: actions?.onShow }, { label: 'Mostrar o Resenhacord', click: actions?.onShow },
{ label: 'Hide', click: actions?.onHide }, { label: 'Hide', click: actions?.onHide },
{ type: 'separator' }, { type: 'separator' },
checkForUpdatesItem(state, () => actions?.onCheckForUpdates?.()), checkForUpdatesItem(state, () => actions?.onCheckForUpdates?.()),
@@ -419,7 +428,8 @@ export function handleRecoveryAction(action: RecoveryAction): void {
// Direct quitAndInstall — does NOT rely on autoInstallOnAppQuit. // Direct quitAndInstall — does NOT rely on autoInstallOnAppQuit.
// Force-kill-fix: if user reaches recovery and clicks here, install // Force-kill-fix: if user reaches recovery and clicks here, install
// happens cleanly even if the on-quit hook would otherwise be bypassed. // happens cleanly even if the on-quit hook would otherwise be bypassed.
autoUpdaterRef?.quitAndInstall(); // Silent + relaunch, matching the in-app banner: no installer wizard.
autoUpdaterRef?.quitAndInstall(true, true);
return; return;
} }
case 'change-instance': { case 'change-instance': {
@@ -440,7 +450,7 @@ export function handleRecoveryAction(action: RecoveryAction): void {
return; return;
} }
case 'open-releases': { case 'open-releases': {
shell.openExternal('https://github.com/TheZwiss/backspace/releases/latest'); shell.openExternal(RELEASES_URL);
return; return;
} }
case 'quit': { case 'quit': {
+7
View File
@@ -0,0 +1,7 @@
node_modules/
# Gerados por `cap sync` a cada build — versioná-los só criaria diffs ruidosos
# e o risco de o APK sair com uma cópia velha.
android/app/src/main/assets/public/
android/app/src/main/assets/capacitor.config.json
android/app/src/main/assets/capacitor.plugins.json
+101
View File
@@ -0,0 +1,101 @@
# Using Android gitignore template: https://github.com/github/gitignore/blob/HEAD/Android.gitignore
# Built application files
*.apk
*.aar
*.ap_
*.aab
# Files for the ART/Dalvik VM
*.dex
# Java class files
*.class
# Generated files
bin/
gen/
out/
# Uncomment the following line in case you need and you don't have the release build type files in your app
# release/
# Gradle files
.gradle/
build/
# Local configuration file (sdk path, etc)
local.properties
# Proguard folder generated by Eclipse
proguard/
# Log Files
*.log
# Android Studio Navigation editor temp files
.navigation/
# Android Studio captures folder
captures/
# IntelliJ
*.iml
.idea/workspace.xml
.idea/tasks.xml
.idea/gradle.xml
.idea/assetWizardSettings.xml
.idea/dictionaries
.idea/libraries
# Android Studio 3 in .gitignore file.
.idea/caches
.idea/modules.xml
# Comment next line if keeping position of elements in Navigation Editor is relevant for you
.idea/navEditor.xml
# Keystore files
# Uncomment the following lines if you do not want to check your keystore files in.
#*.jks
#*.keystore
# External native build folder generated in Android Studio 2.2 and later
.externalNativeBuild
.cxx/
# Google Services (e.g. APIs or Firebase)
# google-services.json
# Freeline
freeline.py
freeline/
freeline_project_description.json
# fastlane
fastlane/report.xml
fastlane/Preview.html
fastlane/screenshots
fastlane/test_output
fastlane/readme.md
# Version control
vcs.xml
# lint
lint/intermediates/
lint/generated/
lint/outputs/
lint/tmp/
# lint/reports/
# Android Profiling
*.hprof
# Cordova plugins for Capacitor
capacitor-cordova-android-plugins
# Copied web assets
app/src/main/assets/public
# Generated Config files
app/src/main/assets/capacitor.config.json
app/src/main/assets/capacitor.plugins.json
app/src/main/res/xml/config.xml
+2
View File
@@ -0,0 +1,2 @@
/build/*
!/build/.npmkeep
+57
View File
@@ -0,0 +1,57 @@
apply plugin: 'com.android.application'
android {
namespace "com.resenhacord.app"
compileSdk rootProject.ext.compileSdkVersion
defaultConfig {
applicationId "com.resenhacord.app"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
// Mantidos em sincronia com a versao do monorepo. O versionCode e o
// numero que o Android usa para decidir se um APK e mais novo, e ele
// precisa ser inteiro e sempre crescente: 2.1.0 -> 20100.
versionCode 20100
versionName "2.1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
aaptOptions {
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
// Default: https://android.googlesource.com/platform/frameworks/base/+/282e181b58cf72b6ca770dc7ca5f91f135444502/tools/aapt/AaptAssets.cpp#61
ignoreAssetsPattern '!.svn:!.git:!.ds_store:!*.scc:.*:!CVS:!thumbs.db:!picasa.ini:!*~'
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
repositories {
flatDir{
dirs '../capacitor-cordova-android-plugins/src/main/libs', 'libs'
}
}
dependencies {
implementation fileTree(include: ['*.jar'], dir: 'libs')
implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion"
implementation "androidx.coordinatorlayout:coordinatorlayout:$androidxCoordinatorLayoutVersion"
implementation "androidx.core:core-splashscreen:$coreSplashScreenVersion"
implementation project(':capacitor-android')
testImplementation "junit:junit:$junitVersion"
androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"
implementation project(':capacitor-cordova-android-plugins')
}
apply from: 'capacitor.build.gradle'
try {
def servicesJSON = file('google-services.json')
if (servicesJSON.text) {
apply plugin: 'com.google.gms.google-services'
}
} catch(Exception e) {
logger.info("google-services.json not found, google-services plugin not applied. Push Notifications won't work")
}
@@ -0,0 +1,19 @@
// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN
android {
compileOptions {
sourceCompatibility JavaVersion.VERSION_21
targetCompatibility JavaVersion.VERSION_21
}
}
apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle"
dependencies {
}
if (hasProperty('postBuildExtras')) {
postBuildExtras()
}
+21
View File
@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
@@ -0,0 +1,26 @@
package com.getcapacitor.myapp;
import static org.junit.Assert.*;
import android.content.Context;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.platform.app.InstrumentationRegistry;
import org.junit.Test;
import org.junit.runner.RunWith;
/**
* Instrumented test, which will execute on an Android device.
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
@RunWith(AndroidJUnit4.class)
public class ExampleInstrumentedTest {
@Test
public void useAppContext() throws Exception {
// Context of the app under test.
Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
assertEquals("com.getcapacitor.app", appContext.getPackageName());
}
}
@@ -0,0 +1,82 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode|navigation"
android:name=".MainActivity"
android:label="@string/title_activity_main"
android:theme="@style/AppTheme.NoActionBarLaunch"
android:launchMode="singleTask"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<!--
O tipo "microphone" tem de bater com o que o servico passa para o
startForeground e com a permissao FOREGROUND_SERVICE_MICROPHONE. A
partir do Android 14 os tres precisam concordar, senao o sistema
derruba o app na hora de entrar na call.
-->
<service
android:name=".VoiceForegroundService"
android:exported="false"
android:foregroundServiceType="microphone" />
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths"></meta-data>
</provider>
</application>
<!-- Permissions -->
<uses-permission android:name="android.permission.INTERNET" />
<!--
Voz e video. A WebView nao pede nada ao sistema sozinha: quando a pagina
chama getUserMedia, o bridge do Capacitor intercepta o pedido e traduz
para estas permissoes nativas. Sem declara-las aqui, a chamada falha com
NotAllowedError e o microfone simplesmente nao liga.
-->
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.CAMERA" />
<!-- Trocar entre alto-falante e fone durante a chamada. -->
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<!-- Android 13+ nao mostra notificacao nenhuma sem esta permissao. -->
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<!--
Chamada com a tela apagada. O servico em primeiro plano impede o sistema
de congelar o processo; o wake lock impede a CPU de dormir. Sem os dois, a
WebView para de rodar o JavaScript e a call cai.
-->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<!--
O aparelho precisa ter microfone; camera e opcional para nao excluir
tablets sem camera frontal da lista de aparelhos compativeis.
-->
<uses-feature android:name="android.hardware.microphone" android:required="true" />
<uses-feature android:name="android.hardware.camera" android:required="false" />
</manifest>
@@ -0,0 +1,36 @@
package com.resenhacord.app;
import android.Manifest;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Bundle;
import com.getcapacitor.BridgeActivity;
public class MainActivity extends BridgeActivity {
private static final int REQ_NOTIFICATIONS = 100;
@Override
public void onCreate(Bundle savedInstanceState) {
// Plugins que moram no proprio app nao sao descobertos sozinhos, e o
// registro tem de acontecer antes do super.onCreate — depois disso a
// ponte ja foi montada e o plugin nao aparece para o JavaScript.
registerPlugin(VoicePlugin.class);
super.onCreate(savedInstanceState);
requestNotificationPermission();
}
/**
* A partir do Android 13 a notificacao da chamada nao aparece sem esta
* permissao. O servico continua rodando de qualquer forma — a chamada nao
* cai se o usuario recusar —, mas ele fica sem nada visivel na barra, o que
* so parece defeito. Por isso perguntamos, e seguimos com a recusa.
*/
private void requestNotificationPermission() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) return;
if (checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS)
== PackageManager.PERMISSION_GRANTED) return;
requestPermissions(new String[]{Manifest.permission.POST_NOTIFICATIONS}, REQ_NOTIFICATIONS);
}
}
@@ -0,0 +1,151 @@
package com.resenhacord.app;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ServiceInfo;
import android.os.Build;
import android.os.IBinder;
import android.os.PowerManager;
/**
* Mantem o app vivo enquanto ha uma chamada em andamento.
*
* Sem isto, trancar a tela derruba a call. O Android congela processos em
* segundo plano — e uma WebView congelada para de rodar o JavaScript que
* mantem a conexao do LiveKit de pe. Um servico em primeiro plano e a unica
* coisa que o sistema aceita como "este app precisa continuar rodando", e ele
* exige a notificacao permanente que aparece junto: e o preco combinado, o
* usuario sempre ve que algo esta ativo.
*
* O wake lock parcial e separado e igualmente necessario: o servico impede o
* congelamento do processo, mas nao impede a CPU de dormir.
*/
public class VoiceForegroundService extends Service {
public static final String ACTION_START = "com.resenhacord.app.VOICE_START";
public static final String ACTION_STOP = "com.resenhacord.app.VOICE_STOP";
public static final String ACTION_HANGUP = "com.resenhacord.app.VOICE_HANGUP";
public static final String EXTRA_CHANNEL = "channel";
private static final String CHANNEL_ID = "voice_call";
private static final int NOTIFICATION_ID = 1;
private PowerManager.WakeLock wakeLock;
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
String action = intent != null ? intent.getAction() : null;
if (ACTION_STOP.equals(action)) {
stop();
return START_NOT_STICKY;
}
if (ACTION_HANGUP.equals(action)) {
// O botao da notificacao nao desliga a chamada sozinho: quem sabe
// como sair da sala e o app web. Avisamos o JavaScript e deixamos
// ele encerrar; o servico para quando o JS confirmar a saida.
VoicePlugin.emitHangup();
return START_NOT_STICKY;
}
String channel = intent != null ? intent.getStringExtra(EXTRA_CHANNEL) : null;
startForegroundWithNotification(channel);
acquireWakeLock();
return START_STICKY;
}
private void startForegroundWithNotification(String channelName) {
createNotificationChannel();
Intent open = new Intent(this, MainActivity.class);
open.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
PendingIntent openPending = PendingIntent.getActivity(
this, 0, open, PendingIntent.FLAG_IMMUTABLE);
Intent hangup = new Intent(this, VoiceForegroundService.class);
hangup.setAction(ACTION_HANGUP);
PendingIntent hangupPending = PendingIntent.getService(
this, 1, hangup, PendingIntent.FLAG_IMMUTABLE);
String text = channelName != null && !channelName.isEmpty()
? channelName
: getString(R.string.voice_notification_text);
Notification notification = new Notification.Builder(this, CHANNEL_ID)
.setContentTitle(getString(R.string.voice_notification_title))
.setContentText(text)
.setSmallIcon(R.drawable.ic_stat_voice)
.setContentIntent(openPending)
.addAction(new Notification.Action.Builder(
null, getString(R.string.voice_notification_hangup), hangupPending).build())
.setOngoing(true)
.setUsesChronometer(true)
.build();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
// O tipo e obrigatorio a partir do Android 14: sem ele o sistema
// recusa o servico e derruba o app com uma excecao.
startForeground(NOTIFICATION_ID, notification,
ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE);
} else {
startForeground(NOTIFICATION_ID, notification);
}
}
private void createNotificationChannel() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return;
NotificationManager manager = getSystemService(NotificationManager.class);
if (manager.getNotificationChannel(CHANNEL_ID) != null) return;
// Importancia baixa de proposito: a notificacao existe porque o sistema
// exige, nao para chamar atencao. Som ou vibracao aqui tocariam a cada
// entrada em call, por cima da propria chamada.
NotificationChannel channel = new NotificationChannel(
CHANNEL_ID,
getString(R.string.voice_channel_name),
NotificationManager.IMPORTANCE_LOW);
channel.setDescription(getString(R.string.voice_channel_description));
channel.setShowBadge(false);
manager.createNotificationChannel(channel);
}
private void acquireWakeLock() {
if (wakeLock != null && wakeLock.isHeld()) return;
PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "Resenhacord::voice");
// Sem timeout infinito: se o app morrer de um jeito que nao passe pelo
// stop(), o sistema solta o lock sozinho depois de duas horas em vez de
// segurar a CPU para sempre.
wakeLock.acquire(2 * 60 * 60 * 1000L);
}
private void releaseWakeLock() {
if (wakeLock != null && wakeLock.isHeld()) {
wakeLock.release();
}
wakeLock = null;
}
private void stop() {
releaseWakeLock();
stopForeground(true);
stopSelf();
}
@Override
public void onDestroy() {
releaseWakeLock();
super.onDestroy();
}
}
@@ -0,0 +1,66 @@
package com.resenhacord.app;
import android.content.Intent;
import android.os.Build;
import com.getcapacitor.JSObject;
import com.getcapacitor.Plugin;
import com.getcapacitor.PluginCall;
import com.getcapacitor.PluginMethod;
import com.getcapacitor.annotation.CapacitorPlugin;
import java.lang.ref.WeakReference;
/**
* Ponte entre o app web e o servico de chamada.
*
* O app web e quem sabe quando ha uma call: ele fala com o LiveKit. O lado
* nativo e quem sabe pedir ao Android para nao congelar o processo. Este plugin
* so liga as duas pontas.
*/
@CapacitorPlugin(name = "VoiceForeground")
public class VoicePlugin extends Plugin {
/**
* Referencia fraca de proposito: o servico precisa avisar o JavaScript
* quando alguem toca em "Sair" na notificacao, mas ele nao pode ser o
* motivo de a instancia do plugin (e a Activity presa a ela) continuar
* viva depois de destruida.
*/
private static WeakReference<VoicePlugin> instance = new WeakReference<>(null);
@Override
public void load() {
instance = new WeakReference<>(this);
}
@PluginMethod
public void start(PluginCall call) {
Intent intent = new Intent(getContext(), VoiceForegroundService.class);
intent.setAction(VoiceForegroundService.ACTION_START);
intent.putExtra(VoiceForegroundService.EXTRA_CHANNEL, call.getString("channel"));
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
getContext().startForegroundService(intent);
} else {
getContext().startService(intent);
}
call.resolve();
}
@PluginMethod
public void stop(PluginCall call) {
Intent intent = new Intent(getContext(), VoiceForegroundService.class);
intent.setAction(VoiceForegroundService.ACTION_STOP);
getContext().startService(intent);
call.resolve();
}
/** Chamado pelo servico quando o botao "Sair" da notificacao e tocado. */
static void emitHangup() {
VoicePlugin plugin = instance.get();
if (plugin != null) {
plugin.notifyListeners("hangup", new JSObject());
}
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

@@ -0,0 +1,34 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportHeight="108"
android:viewportWidth="108">
<path
android:fillType="evenOdd"
android:pathData="M32,64C32,64 38.39,52.99 44.13,50.95C51.37,48.37 70.14,49.57 70.14,49.57L108.26,87.69L108,109.01L75.97,107.97L32,64Z"
android:strokeColor="#00000000"
android:strokeWidth="1">
<aapt:attr name="android:fillColor">
<gradient
android:endX="78.5885"
android:endY="90.9159"
android:startX="48.7653"
android:startY="61.0927"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M66.94,46.02L66.94,46.02C72.44,50.07 76,56.61 76,64L32,64C32,56.61 35.56,50.11 40.98,46.06L36.18,41.19C35.45,40.45 35.45,39.3 36.18,38.56C36.91,37.81 38.05,37.81 38.78,38.56L44.25,44.05C47.18,42.57 50.48,41.71 54,41.71C57.48,41.71 60.78,42.57 63.68,44.05L69.11,38.56C69.84,37.81 70.98,37.81 71.71,38.56C72.44,39.3 72.44,40.45 71.71,41.19L66.94,46.02ZM62.94,56.92C64.08,56.92 65,56.01 65,54.88C65,53.76 64.08,52.85 62.94,52.85C61.8,52.85 60.88,53.76 60.88,54.88C60.88,56.01 61.8,56.92 62.94,56.92ZM45.06,56.92C46.2,56.92 47.13,56.01 47.13,54.88C47.13,53.76 46.2,52.85 45.06,52.85C43.92,52.85 43,53.76 43,54.88C43,56.01 43.92,56.92 45.06,56.92Z"
android:strokeColor="#00000000"
android:strokeWidth="1" />
</vector>
@@ -0,0 +1,170 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportHeight="108"
android:viewportWidth="108">
<path
android:fillColor="#26A69A"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
</vector>
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Icone pequeno da notificacao. O Android descarta a cor e usa so o formato
(a partir do Lollipop ele e pintado de branco sobre a silhueta), entao aqui
vale desenho solido e simples — um microfone.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="#FFFFFF">
<path
android:fillColor="#FFFFFF"
android:pathData="M12,14c1.66,0 3,-1.34 3,-3V5c0,-1.66 -1.34,-3 -3,-3S9,3.34 9,5v6c0,1.66 1.34,3 3,3z"/>
<path
android:fillColor="#FFFFFF"
android:pathData="M17,11c0,2.76 -2.24,5 -5,5s-5,-2.24 -5,-5H5c0,3.53 2.61,6.43 6,6.92V21h2v-3.08c3.39,-0.49 6,-3.39 6,-6.92h-2z"/>
</vector>
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Tela de abertura como camadas em vez das splash.png que o Capacitor gera para
cada densidade e orientacao. Duas razoes: a cor de fundo fica exata em
qualquer aparelho (as PNGs padrao sao claras e piscavam branco antes da
WebView pintar), e a marca escala sozinha sem manter doze arquivos.
-->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@color/brand_background" />
<item
android:drawable="@drawable/brand_mark"
android:gravity="center"
android:width="128dp"
android:height="128dp" />
</layer-list>
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<WebView
android:layout_width="match_parent"
android:layout_height="match_parent" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- theme_color / background_color do manifest do PWA, para a abertura
nativa e a WebView comecarem na mesma cor. -->
<color name="brand_background">#0B0B10</color>
<color name="colorPrimary">#0B0B10</color>
<color name="colorPrimaryDark">#0B0B10</color>
<color name="colorAccent">#A78BFA</color>
</resources>
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- A mesma base escura da marca; o icone adaptativo recorta o primeiro
plano sobre ela em qualquer formato que o launcher use. -->
<color name="ic_launcher_background">#0B0B10</color>
</resources>
@@ -0,0 +1,12 @@
<?xml version='1.0' encoding='utf-8'?>
<resources>
<string name="app_name">Resenhacord</string>
<string name="title_activity_main">Resenhacord</string>
<string name="package_name">com.resenhacord.app</string>
<string name="custom_url_scheme">com.resenhacord.app</string>
<string name="voice_channel_name">Chamada em andamento</string>
<string name="voice_channel_description">Mantem a chamada de voz ativa com o app em segundo plano</string>
<string name="voice_notification_title">Resenhacord</string>
<string name="voice_notification_text">Em call</string>
<string name="voice_notification_hangup">Sair</string>
</resources>
@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
</style>
<style name="AppTheme.NoActionBar" parent="Theme.AppCompat.DayNight.NoActionBar">
<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
<item name="android:background">@null</item>
</style>
<style name="AppTheme.NoActionBarLaunch" parent="Theme.SplashScreen">
<item name="android:background">@drawable/splash</item>
</style>
</resources>
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<external-path name="my_images" path="." />
<cache-path name="my_cache_images" path="." />
</paths>
@@ -0,0 +1,18 @@
package com.getcapacitor.myapp;
import static org.junit.Assert.*;
import org.junit.Test;
/**
* Example local unit test, which will execute on the development machine (host).
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
public class ExampleUnitTest {
@Test
public void addition_isCorrect() throws Exception {
assertEquals(4, 2 + 2);
}
}
+29
View File
@@ -0,0 +1,29 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:8.7.2'
classpath 'com.google.gms:google-services:4.4.2'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
apply from: "variables.gradle"
allprojects {
repositories {
google()
mavenCentral()
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}
@@ -0,0 +1,3 @@
// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN
include ':capacitor-android'
project(':capacitor-android').projectDir = new File('../node_modules/@capacitor/android/capacitor')
+22
View File
@@ -0,0 +1,22 @@
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx1536m
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
# AndroidX package structure to make it clearer which packages are bundled with the
# Android operating system, and which are packaged with your app's APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true
Binary file not shown.
@@ -0,0 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-all.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
+252
View File
@@ -0,0 +1,252 @@
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"
+94
View File
@@ -0,0 +1,94 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
+5
View File
@@ -0,0 +1,5 @@
include ':app'
include ':capacitor-cordova-android-plugins'
project(':capacitor-cordova-android-plugins').projectDir = new File('./capacitor-cordova-android-plugins/')
apply from: 'capacitor.settings.gradle'
+16
View File
@@ -0,0 +1,16 @@
ext {
minSdkVersion = 23
compileSdkVersion = 35
targetSdkVersion = 35
androidxActivityVersion = '1.9.2'
androidxAppCompatVersion = '1.7.0'
androidxCoordinatorLayoutVersion = '1.2.0'
androidxCoreVersion = '1.15.0'
androidxFragmentVersion = '1.8.4'
coreSplashScreenVersion = '1.0.1'
androidxWebkitVersion = '1.12.1'
junitVersion = '4.13.2'
androidxJunitVersion = '1.2.1'
androidxEspressoCoreVersion = '3.6.1'
cordovaAndroidVersion = '10.1.1'
}
+41
View File
@@ -0,0 +1,41 @@
import type { CapacitorConfig } from '@capacitor/cli';
/**
* O APK e uma casca fina: a WebView carrega a instancia pela rede em vez de
* servir uma copia do app web embutida no pacote.
*
* Duas razoes, nesta ordem:
*
* 1. Atualizacao. O site muda a cada deploy. Com os arquivos embutidos, cada
* mudanca no app web exigiria republicar e reinstalar o APK a mao — nao ha
* updater automatico no Android como o electron-updater faz no desktop.
* Pela rede, o telefone recebe a mesma versao que o navegador, na hora.
*
* 2. Origem. O cliente fala com /api e /ws na mesma origem. Embutido, a pagina
* rodaria em https://localhost e toda chamada viraria cross-origin, o que
* obrigaria a abrir CORS no servidor para uma origem que qualquer app pode
* reivindicar.
*
* O preco e nao funcionar offline, o que para um app de chat em tempo real nao
* e perda: sem servidor nao ha o que mostrar. A pagina em public/ so aparece se
* a WebView nao alcancar o servidor.
*/
const config: CapacitorConfig = {
appId: 'com.resenhacord.app',
appName: 'Resenhacord',
webDir: 'public',
android: {
// A captura de audio e video da WebView e concedida pelo bridge do
// Capacitor, que traduz o pedido do getUserMedia para a permissao nativa.
allowMixedContent: false,
},
server: {
url: 'https://chat.resenha.website',
// Sem cleartext: a instancia e servida por HTTPS (Caddy), e o WebRTC do
// LiveKit exige contexto seguro de qualquer forma.
cleartext: false,
androidScheme: 'https',
},
};
export default config;
+1181
View File
File diff suppressed because it is too large Load Diff
+21
View File
@@ -0,0 +1,21 @@
{
"name": "@backspace/mobile",
"version": "2.1.0",
"private": true,
"license": "AGPL-3.0-only",
"description": "Resenhacord para Android",
"scripts": {
"sync": "cap sync android",
"open": "cap open android",
"build:debug": "cd android && ./gradlew assembleDebug",
"build:release": "cd android && ./gradlew assembleRelease"
},
"dependencies": {
"@capacitor/android": "^7.0.0",
"@capacitor/core": "^7.0.0"
},
"devDependencies": {
"@capacitor/cli": "^7.0.0",
"typescript": "^5.7.2"
}
}
+18
View File
@@ -0,0 +1,18 @@
<!doctype html>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>Resenhacord</title>
<style>
:root { color-scheme: dark; }
body {
margin: 0; min-height: 100dvh;
display: grid; place-items: center; padding: 24px;
background: #0b0b10; color: #e7e7ee;
font: 15px/1.5 system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
text-align: center;
}
h1 { font-size: 19px; margin: 0 0 8px; }
p { margin: 0; color: #9a9aab; max-width: 30ch; }
</style>
<h1>Sem conexão</h1>
<p>Não deu pra falar com o servidor. Confere a internet e abre de novo.</p>
@@ -0,0 +1,4 @@
ALTER TABLE `messages` ADD `pinned_at` integer;--> statement-breakpoint
ALTER TABLE `messages` ADD `pinned_by` text;--> statement-breakpoint
CREATE INDEX `idx_messages_channel_pinned` ON `messages` (`channel_id`,`pinned_at`);--> statement-breakpoint
CREATE INDEX `idx_messages_channel_user_created` ON `messages` (`channel_id`,`user_id`,`created_at`);
@@ -0,0 +1,26 @@
CREATE TABLE `space_emojis` (
`id` text PRIMARY KEY NOT NULL,
`space_id` text NOT NULL,
`name` text NOT NULL,
`filename` text NOT NULL,
`uploader_id` text,
`created_at` integer NOT NULL,
FOREIGN KEY (`space_id`) REFERENCES `spaces`(`id`) ON UPDATE no action ON DELETE cascade,
FOREIGN KEY (`uploader_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE set null
);
--> statement-breakpoint
CREATE TABLE `space_stickers` (
`id` text PRIMARY KEY NOT NULL,
`space_id` text NOT NULL,
`name` text NOT NULL,
`filename` text NOT NULL,
`uploader_id` text,
`created_at` integer NOT NULL,
FOREIGN KEY (`space_id`) REFERENCES `spaces`(`id`) ON UPDATE no action ON DELETE cascade,
FOREIGN KEY (`uploader_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE set null
);
--> statement-breakpoint
ALTER TABLE `messages` ADD `sticker_id` text;--> statement-breakpoint
CREATE INDEX `idx_space_emojis_space` ON `space_emojis` (`space_id`);--> statement-breakpoint
CREATE UNIQUE INDEX `idx_space_emojis_space_name` ON `space_emojis` (`space_id`,`name`);--> statement-breakpoint
CREATE INDEX `idx_space_stickers_space` ON `space_stickers` (`space_id`);
@@ -0,0 +1 @@
ALTER TABLE `messages` ADD `type` text DEFAULT 'user' NOT NULL;
@@ -0,0 +1 @@
ALTER TABLE `users` ADD `profile_style` text;
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -113,6 +113,34 @@
"when": 1788194631751, "when": 1788194631751,
"tag": "0015_young_human_fly", "tag": "0015_young_human_fly",
"breakpoints": true "breakpoints": true
},
{
"idx": 16,
"version": "6",
"when": 1788295165743,
"tag": "0016_lyrical_freak",
"breakpoints": true
},
{
"idx": 17,
"version": "6",
"when": 1788295936825,
"tag": "0017_purple_wildside",
"breakpoints": true
},
{
"idx": 18,
"version": "6",
"when": 1788401450986,
"tag": "0018_eminent_kid_colt",
"breakpoints": true
},
{
"idx": 19,
"version": "6",
"when": 1788402724322,
"tag": "0019_glamorous_crusher_hogan",
"breakpoints": true
} }
] ]
} }

Some files were not shown because too many files have changed in this diff Show More