Add native app shortcuts for common features (#604)

This commit is contained in:
Gaurav Chaudhary 2026-09-21 19:51:15 +00:00 committed by hodlbod
parent 31d9c6bd9c
commit b43309f0c3
11 changed files with 279 additions and 3 deletions

View file

@ -38,6 +38,10 @@
<data android:mimeType="image/*" />
<data android:mimeType="video/*" />
</intent-filter>
<meta-data
android:name="android.app.shortcuts"
android:resource="@xml/shortcuts" />
</activity>
<provider

View file

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="48dp"
android:height="48dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M12,0A12,12 0,1 1,12 24A12,12 0,1 1,12 0Z" />
<group
android:scaleX="0.6666667"
android:scaleY="0.6666667"
android:translateX="4"
android:translateY="4">
<path
android:fillColor="@android:color/transparent"
android:pathData="M2 12.0002C2 7.2862 2 4.92918 3.46447 3.46471C4.92893 2.00024 7.28595 2.00024 12 2.00024C16.714 2.00024 19.0711 2.00024 20.5355 3.46471C22 4.92918 22 7.2862 22 12.0002C22 16.7143 22 19.0713 20.5355 20.5358C19.0711 22.0002 16.714 22.0002 12 22.0002C7.28595 22.0002 4.92893 22.0002 3.46447 20.5358C2 19.0713 2 16.7143 2 12.0002Z"
android:strokeColor="#FF202124"
android:strokeWidth="1.5" />
<path
android:fillColor="@android:color/transparent"
android:pathData="M2 13.0002H5.16026C6.06543 13.0002 6.51802 13.0002 6.91584 13.1832C7.31367 13.3662 7.60821 13.7098 8.19729 14.3971L8.80271 15.1034C9.39179 15.7907 9.68633 16.1343 10.0842 16.3173C10.482 16.5002 10.9346 16.5002 11.8397 16.5002H12.1603C13.0654 16.5002 13.518 16.5002 13.9158 16.3173C14.3137 16.1343 14.6082 15.7907 15.1973 15.1034L15.8027 14.3971C16.3918 13.7098 16.6863 13.3662 17.0842 13.1832C17.482 13.0002 17.9346 13.0002 18.8397 13.0002H22"
android:strokeColor="#FF202124"
android:strokeWidth="1.5"
android:strokeLineCap="round" />
</group>
</vector>

View file

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="48dp"
android:height="48dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M12,0A12,12 0,1 1,12 24A12,12 0,1 1,12 0Z" />
<group
android:scaleX="0.6666667"
android:scaleY="0.6666667"
android:translateX="4"
android:translateY="4">
<path
android:fillColor="@android:color/transparent"
android:pathData="M2 12.0002C2 8.22901 2 6.34339 3.17157 5.17182C4.34315 4.00024 6.22876 4.00024 10 4.00024H14C17.7712 4.00024 19.6569 4.00024 20.8284 5.17182C22 6.34339 22 8.22901 22 12.0002C22 15.7715 22 17.6571 20.8284 18.8287C19.6569 20.0002 17.7712 20.0002 14 20.0002H10C6.22876 20.0002 4.34315 20.0002 3.17157 18.8287C2 17.6571 2 15.7715 2 12.0002Z"
android:strokeColor="#FF202124"
android:strokeWidth="1.5" />
<path
android:fillColor="@android:color/transparent"
android:pathData="M6 8.00024L8.1589 9.79932C9.99553 11.3299 10.9139 12.0951 12 12.0951C13.0861 12.0951 14.0045 11.3299 15.8411 9.79932L18 8.00024"
android:strokeColor="#FF202124"
android:strokeWidth="1.5"
android:strokeLineCap="round" />
</group>
</vector>

View file

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="48dp"
android:height="48dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M12,0A12,12 0,1 1,12 24A12,12 0,1 1,12 0Z" />
<group
android:scaleX="0.6666667"
android:scaleY="0.6666667"
android:translateX="4"
android:translateY="4">
<path
android:fillColor="@android:color/transparent"
android:pathData="M2,11.5005a9.5,9.5 0,1 1,19 0a9.5,9.5 0,1 1,-19 0"
android:strokeColor="#FF202124"
android:strokeWidth="1.5" />
<path
android:fillColor="@android:color/transparent"
android:pathData="M18.5 18.5005L22 22.0005"
android:strokeColor="#FF202124"
android:strokeWidth="1.5"
android:strokeLineCap="round" />
</group>
</vector>

View file

@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="48dp"
android:height="48dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M12,0A12,12 0,1 1,12 24A12,12 0,1 1,12 0Z" />
<group
android:scaleX="0.6666667"
android:scaleY="0.6666667"
android:translateX="4"
android:translateY="4">
<path
android:fillColor="@android:color/transparent"
android:pathData="M2.5 6.50049C2.5 4.61487 2.5 3.67206 3.08579 3.08627C3.67157 2.50049 4.61438 2.50049 6.5 2.50049C8.38562 2.50049 9.32843 2.50049 9.91421 3.08627C10.5 3.67206 10.5 4.61487 10.5 6.50049V17.5005C10.5 19.3861 10.5 20.3289 9.91421 20.9147C9.32843 21.5005 8.38562 21.5005 6.5 21.5005C4.61438 21.5005 3.67157 21.5005 3.08579 20.9147C2.5 20.3289 2.5 19.3861 2.5 17.5005V6.50049Z"
android:strokeColor="#FF202124"
android:strokeWidth="1.5" />
<path
android:fillColor="@android:color/transparent"
android:pathData="M13.5 15.5005C13.5 13.6149 13.5 12.6721 14.0858 12.0863C14.6716 11.5005 15.6144 11.5005 17.5 11.5005C19.3856 11.5005 20.3284 11.5005 20.9142 12.0863C21.5 12.6721 21.5 13.6149 21.5 15.5005V17.5005C21.5 19.3861 21.5 20.3289 20.9142 20.9147C20.3284 21.5005 19.3856 21.5005 17.5 21.5005C15.6144 21.5005 14.6716 21.5005 14.0858 20.9147C13.5 20.3289 13.5 19.3861 13.5 17.5005V15.5005Z"
android:strokeColor="#FF202124"
android:strokeWidth="1.5" />
<path
android:fillColor="@android:color/transparent"
android:pathData="M13.5 5.50049C13.5 4.56861 13.5 4.10266 13.6522 3.73512C13.8552 3.24507 14.2446 2.85572 14.7346 2.65273C15.1022 2.50049 15.5681 2.50049 16.5 2.50049H18.5C19.4319 2.50049 19.8978 2.50049 20.2654 2.65273C20.7554 2.85572 21.1448 3.24507 21.3478 3.73512C21.5 4.10266 21.5 4.56861 21.5 5.50049C21.5 6.43237 21.5 6.89831 21.3478 7.26586C21.1448 7.75591 20.7554 8.14526 20.2654 8.34825C19.8978 8.50049 19.4319 8.50049 18.5 8.50049H16.5C15.5681 8.50049 15.1022 8.50049 14.7346 8.34825C14.2446 8.14526 13.8552 7.75591 13.6522 7.26586C13.5 6.89831 13.5 6.43237 13.5 5.50049Z"
android:strokeColor="#FF202124"
android:strokeWidth="1.5" />
</group>
</vector>

View file

@ -5,4 +5,8 @@
<string name="package_name">social.flotilla</string>
<string name="custom_url_scheme">social.flotilla</string>
<string name="default_notification_channel_id">flotilla_push</string>
<string name="shortcut_messages">Messages</string>
<string name="shortcut_search">Search</string>
<string name="shortcut_spaces">Spaces</string>
<string name="shortcut_inbox">Inbox</string>
</resources>

View file

@ -0,0 +1,47 @@
<?xml version="1.0" encoding="utf-8"?>
<shortcuts xmlns:android="http://schemas.android.com/apk/res/android">
<shortcut
android:shortcutId="messages"
android:enabled="true"
android:icon="@drawable/shortcut_messages"
android:shortcutShortLabel="@string/shortcut_messages">
<intent
android:action="android.intent.action.VIEW"
android:targetPackage="social.flotilla"
android:targetClass="social.flotilla.MainActivity"
android:data="flotilla://shortcut/messages" />
</shortcut>
<shortcut
android:shortcutId="search"
android:enabled="true"
android:icon="@drawable/shortcut_search"
android:shortcutShortLabel="@string/shortcut_search">
<intent
android:action="android.intent.action.VIEW"
android:targetPackage="social.flotilla"
android:targetClass="social.flotilla.MainActivity"
android:data="flotilla://shortcut/search" />
</shortcut>
<shortcut
android:shortcutId="spaces"
android:enabled="true"
android:icon="@drawable/shortcut_spaces"
android:shortcutShortLabel="@string/shortcut_spaces">
<intent
android:action="android.intent.action.VIEW"
android:targetPackage="social.flotilla"
android:targetClass="social.flotilla.MainActivity"
android:data="flotilla://shortcut/spaces" />
</shortcut>
<shortcut
android:shortcutId="inbox"
android:enabled="true"
android:icon="@drawable/shortcut_inbox"
android:shortcutShortLabel="@string/shortcut_inbox">
<intent
android:action="android.intent.action.VIEW"
android:targetPackage="social.flotilla"
android:targetClass="social.flotilla.MainActivity"
android:data="flotilla://shortcut/inbox" />
</shortcut>
</shortcuts>

View file

@ -1,9 +1,9 @@
import assert from "node:assert/strict"
import {appendFile, cp, readFile, rm, writeFile} from "node:fs/promises"
const replace = async (file, pattern, replacement) => {
const replace = async (file, pattern, replacement, count = 1) => {
const source = await readFile(file, "utf8")
assert.equal([...source.matchAll(pattern)].length, 1, `Expected one match in ${file}`)
assert.equal([...source.matchAll(pattern)].length, count, `Expected ${count} matches in ${file}`)
await writeFile(file, source.replace(pattern, replacement))
}
@ -59,6 +59,12 @@ await replace(
/\n {8}<!-- FCM uses[\s\S]*?android:value="@string\/default_notification_channel_id"\n {8}\/>/g,
"",
)
await replace(
"android/app/src/main/res/xml/shortcuts.xml",
/android:targetPackage="social\.flotilla"/g,
'android:targetPackage="social.flotilla.fdroid"',
4,
)
await replace(
"src/app.html",
/\s*<script\s+defer\s+data-domain="[^"]*"\s+src="https:\/\/plausible\.coracle\.social\/[^"]*"><\/script>/g,

View file

@ -7,10 +7,49 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
if let shortcutItem = launchOptions?[.shortcutItem] as? UIApplicationShortcutItem,
let url = shortcutURL(for: shortcutItem.type) {
// The proxy retains lastURL for App.getLaunchUrl() before the bridge is ready.
_ = ApplicationDelegateProxy.shared.application(application, open: url, options: [:])
return false
}
// Override point for customization after application launch.
return true
}
func application(
_ application: UIApplication,
performActionFor shortcutItem: UIApplicationShortcutItem,
completionHandler: @escaping (Bool) -> Void
) {
guard let url = shortcutURL(for: shortcutItem.type) else {
completionHandler(false)
return
}
completionHandler(ApplicationDelegateProxy.shared.application(application, open: url, options: [:]))
}
private func shortcutURL(for type: String) -> URL? {
let path: String
switch type {
case "social.flotilla.shortcut.messages":
path = "messages"
case "social.flotilla.shortcut.search":
path = "search"
case "social.flotilla.shortcut.spaces":
path = "spaces"
case "social.flotilla.shortcut.inbox":
path = "inbox"
default:
return nil
}
return URL(string: "flotilla://shortcut/\(path)")
}
func applicationWillResignActive(_ application: UIApplication) {
// Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
// Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game.

View file

@ -31,6 +31,41 @@
<string>$(MARKETING_VERSION)</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>UIApplicationShortcutItems</key>
<array>
<dict>
<key>UIApplicationShortcutItemType</key>
<string>social.flotilla.shortcut.messages</string>
<key>UIApplicationShortcutItemTitle</key>
<string>Messages</string>
<key>UIApplicationShortcutItemIconSymbolName</key>
<string>message</string>
</dict>
<dict>
<key>UIApplicationShortcutItemType</key>
<string>social.flotilla.shortcut.search</string>
<key>UIApplicationShortcutItemTitle</key>
<string>Search</string>
<key>UIApplicationShortcutItemIconSymbolName</key>
<string>magnifyingglass</string>
</dict>
<dict>
<key>UIApplicationShortcutItemType</key>
<string>social.flotilla.shortcut.spaces</string>
<key>UIApplicationShortcutItemTitle</key>
<string>Spaces</string>
<key>UIApplicationShortcutItemIconSymbolName</key>
<string>square.grid.2x2</string>
</dict>
<dict>
<key>UIApplicationShortcutItemType</key>
<string>social.flotilla.shortcut.inbox</string>
<key>UIApplicationShortcutItemTitle</key>
<string>Inbox</string>
<key>UIApplicationShortcutItemIconSymbolName</key>
<string>tray</string>
</dict>
</array>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>LSRequiresIPhoneOS</key>

View file

@ -21,7 +21,9 @@
import AppContainer from "@app/components/AppContainer.svelte"
import ModalContainer from "@app/components/ModalContainer.svelte"
import * as core from "@app/core"
import {setupHistory} from "@app/routes"
import {goToChat, goToHome, setupHistory} from "@app/routes"
import Search from "@app/components/Search.svelte"
import {clearModals, getModal, navigate, pushModal} from "@app/modal"
import {setupAnalytics} from "@app/analytics"
import {setupLogging} from "@app/logger"
import "@app/policies"
@ -84,6 +86,33 @@
return
}
if (url.protocol === "flotilla:" && url.host === "shortcut") {
if (!core.app.get().user) return
switch (url.pathname) {
case "/messages":
goToChat()
break
case "/search": {
const modal = getModal()
if (modal?.component !== Search) {
pushModal(Search, {}, {replaceState: Boolean(modal)})
}
break
}
case "/spaces":
navigate("/spaces")
break
case "/inbox":
clearModals()
goToHome()
break
}
return
}
// The iOS share extension can't talk to us directly, so it hands its payload over as a
// flotilla://share url
if (url.host === "share") {