feat(mobile): add native iOS toast module with SPIndicator

Signed-off-by: Innei <tukon479@gmail.com>
This commit is contained in:
Innei 2025-02-21 16:52:09 +08:00
parent 7cfa888a9e
commit 8dbb6343fb
No known key found for this signature in database
GPG Key ID: 0F62D33977F021F7
8 changed files with 121 additions and 36 deletions

View File

@ -1,7 +1,7 @@
{
"platforms": ["apple", "android"],
"apple": {
"modules": ["SharedWebViewModule", "HelperModule"]
"modules": ["SharedWebViewModule", "HelperModule", "ToasterModule"]
},
"android": {
"modules": []

View File

@ -21,6 +21,7 @@ Pod::Spec.new do |s|
s.dependency 'ExpoModulesCore'
s.dependency 'SnapKit', '~> 5.7.0'
s.dependency 'SDWebImage', '~> 5.0'
s.dependency 'SPIndicator', '~> 1.0.0'
# Swift/Objective-C compatibility
s.pod_target_xcconfig = {
'DEFINES_MODULE' => 'YES',

View File

@ -0,0 +1,63 @@
//
// ToasterModule.swift
//
// Created by Innei on 2025/2/21.
//
import ExpoModulesCore
import SPIndicator
enum ToastType: String, Enumerable {
case error
case info
case warn
case success
}
extension ToastType {
func type() -> SPIndicatorIconPreset {
switch self
{
case .error: .error
case .warn: .custom(UIImage(systemName: "exclamationmark.triangle")!.withTintColor(.orange))
case .info: .custom(UIImage(systemName: "info.circle")!.withTintColor(.blue))
case .success: .done
}
}
func haptic() -> SPIndicatorHaptic {
switch self {
case .error: .error
case .info: .success
case .warn: .warning
case .success: .error
}
}
}
struct ToastOptions: Record {
@Field
var message: String?
@Field
var type: ToastType = .info
@Field
var duration: Double = 1.5
@Field
var title: String
}
public class ToasterModule: Module {
public func definition() -> ModuleDefinition {
Name("Toaster")
Function("toast") { (value: ToastOptions) in
DispatchQueue.main.sync {
let indicatorView = SPIndicatorView(
title: value.title, message: value.message, preset: value.type.type())
indicatorView.present(duration: value.duration, haptic: value.type.haptic())
}
}
}
}

View File

@ -1,3 +1,6 @@
import { requireNativeModule } from "expo"
import { Platform } from "react-native"
import { ToastManager } from "../components/ui/toast/manager"
import type { ToastProps } from "../components/ui/toast/types"
@ -11,11 +14,32 @@ type Toast = {
success: (message: string, options?: CommandToastOptions) => void
info: (message: string, options?: CommandToastOptions) => void
}
interface NativeToasterOptions {
title: string
message: string
type: "error" | "success" | "info" | "warn"
/**
* seconds
*/
duration?: number
}
export const toast = {
show: toastInstance.show.bind(toastInstance),
} as Toast
;(["error", "success", "info"] as const).forEach((type) => {
toast[type] = (message: string, options: CommandToastOptions = {}) => {
if (Platform.OS === "ios") {
const NativeToaster = requireNativeModule("Toaster")
NativeToaster.toast({
title: "",
message,
type,
duration: options.duration ? options.duration / 1000 : 1.5,
} as NativeToasterOptions)
return
}
toastInstance.show({
type,
message,

View File

@ -101,11 +101,7 @@ export default function DebugPanel() {
{
title: "Toast",
onPress: () => {
toast.show({
message: "Hello, world!".repeat(10),
type: "success",
variant: "center-replace",
})
toast.error("Hello, world!".repeat(10))
},
},
{

View File

@ -6,7 +6,7 @@ import { useWhoami } from "@/src/store/user/hooks"
export default function LoginPage() {
const whoami = useWhoami()
if (whoami?.id) {
if (whoami?.id && !__DEV__) {
return <Redirect href="/" />
}

View File

@ -7,5 +7,6 @@ export default function Index() {
useEffect(() => {
prepareEntryRenderWebView()
}, [])
return <EntryList />
}

View File

@ -4004,8 +4004,8 @@ packages:
'@radix-ui/react-avatar@1.1.3':
resolution: {integrity: sha512-Paen00T4P8L8gd9bNsRMw7Cbaz85oxiv+hzomsRZgFm2byltPFDtfcoqlWJ8GyZlIBWgLssJlzLCnKU0G0302g==}
peerDependencies:
'@types/react': 18.3.12
'@types/react-dom': 18.3.1
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
@ -4057,9 +4057,9 @@ packages:
'@radix-ui/react-context-menu@2.2.6':
resolution: {integrity: sha512-aUP99QZ3VU84NPsHeaFt4cQUNgJqFsLLOt/RbbWXszZ6MP0DpDyjkFZORr4RpAEx3sUBk+Kc8h13yGtC5Qw8dg==}
peerDependencies:
'@types/react': 18.3.12
'@types/react-dom': 18.3.1
react: 18.3.1
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
@ -4070,7 +4070,7 @@ packages:
'@radix-ui/react-context@1.1.1':
resolution: {integrity: sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==}
peerDependencies:
'@types/react': npm:@types/react@^18.3.12
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
@ -4101,8 +4101,8 @@ packages:
'@radix-ui/react-dismissable-layer@1.1.5':
resolution: {integrity: sha512-E4TywXY6UsXNRhFrECa5HAvE5/4BFcGyfTyK36gP+pAW1ed7UTK4vKwdr53gAJYwqbfCWC6ATvJa3J3R/9+Qrg==}
peerDependencies:
'@types/react': 18.3.12
'@types/react-dom': 18.3.1
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
@ -4127,7 +4127,7 @@ packages:
'@radix-ui/react-focus-guards@1.1.1':
resolution: {integrity: sha512-pSIwfrT1a6sIoDASCSpFwOasEwKTZWDw/iBdtnqKO7v6FeOzYJ7U53cPzYFVR3geGGXgVHaH+CdngrrAzqUGxg==}
peerDependencies:
'@types/react': npm:@types/react@^18.3.12
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
@ -4184,8 +4184,8 @@ packages:
'@radix-ui/react-menu@2.1.6':
resolution: {integrity: sha512-tBBb5CXDJW3t2mo9WlO7r6GTmWV0F0uzHZVFmlRmYpiSK1CDU5IKojP1pm7oknpBOrFZx/YgBRW9oorPO2S/Lg==}
peerDependencies:
'@types/react': 18.3.12
'@types/react-dom': 18.3.1
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
@ -4223,8 +4223,8 @@ packages:
'@radix-ui/react-popper@1.2.2':
resolution: {integrity: sha512-Rvqc3nOpwseCyj/rgjlJDYAgyfw7OC1tTkKn2ivhaMGcYt8FSBlahHOZak2i3QwkRXUXgGgzeEe2RuqeEHuHgA==}
peerDependencies:
'@types/react': 18.3.12
'@types/react-dom': 18.3.1
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
@ -4236,8 +4236,8 @@ packages:
'@radix-ui/react-portal@1.1.4':
resolution: {integrity: sha512-sn2O9k1rPFYVyKd5LAJfo96JlSGVFpa1fS6UuBJfrZadudiw5tAmru+n1x7aMRQ84qDM71Zh1+SzK5QwU0tJfA==}
peerDependencies:
'@types/react': 18.3.12
'@types/react-dom': 18.3.1
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
@ -4288,8 +4288,8 @@ packages:
'@radix-ui/react-roving-focus@1.1.2':
resolution: {integrity: sha512-zgMQWkNO169GtGqRvYrzb0Zf8NhMHS2DuEB/TiEmVnpr5OqPU3i8lfbxaAmC2J/KYuIQxyoQQ6DxepyXp61/xw==}
peerDependencies:
'@types/react': 18.3.12
'@types/react-dom': 18.3.1
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
@ -4406,7 +4406,7 @@ packages:
'@radix-ui/react-use-callback-ref@1.1.0':
resolution: {integrity: sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==}
peerDependencies:
'@types/react': npm:@types/react@^18.3.12
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
@ -4415,7 +4415,7 @@ packages:
'@radix-ui/react-use-controllable-state@1.1.0':
resolution: {integrity: sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw==}
peerDependencies:
'@types/react': npm:@types/react@^18.3.12
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
@ -4469,8 +4469,8 @@ packages:
'@radix-ui/react-visually-hidden@1.1.2':
resolution: {integrity: sha512-1SzA4ns2M1aRlvxErqhLHsBHoS5eI5UUcI2awAMgGUp4LoaoWOKYmvqDY2s/tltuPkh3Yk77YF/r3IRj+Amx4Q==}
peerDependencies:
'@types/react': 18.3.12
'@types/react-dom': 18.3.1
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
@ -4552,7 +4552,7 @@ packages:
'@react-native-menu/menu@1.2.2':
resolution: {integrity: sha512-Uk65PAhwNkCVBAqJu5t2H9biV+m0JLwJc7m3v2X2A/W8SFJmUqYabBsLH4fOWKI3a7kkR9QDT6HruliIKSfM8w==}
peerDependencies:
react: 18.3.1
react: '*'
react-native: '*'
'@react-native-picker/picker@2.11.0':
@ -5263,7 +5263,7 @@ packages:
'@tanstack/react-query@5.66.7':
resolution: {integrity: sha512-qd3q/tUpF2K1xItfPZddk1k/8pSXnovg41XyCqJgPoyYEirMBtB0sVEVVQ/CsAOngzgWtBPXimVf4q4kM9uO6A==}
peerDependencies:
react: 18.3.1
react: ^18 || ^19
'@tanstack/react-virtual@3.13.0':
resolution: {integrity: sha512-CchF0NlLIowiM2GxtsoKBkXA4uqSnY2KvnXo+kyUFD4a4ll6+J0qzoRsUPMwXV/H26lRsxgJIr/YmjYum2oEjg==}
@ -8486,7 +8486,7 @@ packages:
resolution: {integrity: sha512-LUnfrddmee1xLOkyG2NN1l9xQbtvMX3fbM1brEGHg0SKSndvjod3FQdhTzZEYAariqW2RSxQR8v1IsheIoLQXg==}
peerDependencies:
expo: '*'
react-native: '*'
react-native: npm:react-native@0.77.0
expo-module-scripts@4.0.4:
resolution: {integrity: sha512-ytFufVi7HTFLxxf8fbtz3DJuegq3489WPk9pz8Yqm3tYfZ+6/yufPYxLvk6N8b4yBW05oNr/Cqp7MWLpm0xPcA==}
@ -12422,7 +12422,7 @@ packages:
resolution: {integrity: sha512-sc5qgX4YtM6IxjtosjgQLdsal120MvU+YWs0F2MdgQWijps22AXLDCUoBnZZ8vxVhVyJ2WnnIPrmtVBvVJjSuQ==}
engines: {node: '>= 18.0.0'}
peerDependencies:
react: 18.3.1
react: '*'
react-native: '*'
react-native-gesture-handler: '>=2.0.0'
react-native-reanimated: '>=3.0.0'
@ -12536,7 +12536,7 @@ packages:
resolution: {integrity: sha512-9PPREdwH3tMR8otUK1voiFw9AFNtC37Del2fZwbCMBKjOyClC+f3Fwp7KqUouP0A5c3zjEc3T8TetBGQGPQE9A==}
peerDependencies:
react: '*'
react-native: npm:react-native@0.77.0
react-native: '*'
react-native-gesture-handler: '*'
react-native-reanimated: '*'
@ -12550,7 +12550,7 @@ packages:
resolution: {integrity: sha512-E5N/eK/+HtAVJUAzXpm1cWz8ROheV9jb0TI6h2bM+333U+DWibTTnT2T1122FkCoXLXIYavtm2FR2if+5jH8cA==}
peerDependencies:
react: '>=16.8.6'
react-native: npm:react-native@0.77.0
react-native: '>=0.60.0-rc.2'
react-native-windows: '>=0.63.0'
shaka-player: ^4.7.9
peerDependenciesMeta:
@ -12564,13 +12564,13 @@ packages:
peerDependencies:
nativewind: '>=4.1.0'
react: '>=18.0.0'
react-native: '>=0.76.0'
react-native: npm:react-native@0.77.0
tailwindcss: '>=3.0.0'
react-native-volume-manager@2.0.8:
resolution: {integrity: sha512-aZM47/mYkdQ4CbXpKYO6Ajiczv7fxbQXZ9c0H8gRuQUaS3OCz/MZABer6o9aDWq0KMNsQ7q7GVFLRPnSSeeMmw==}
peerDependencies:
react: 18.3.1
react: '*'
react-native: '*'
react-native-web@0.19.13: