Skip to content

Commit

Permalink
add haptics
Browse files Browse the repository at this point in the history
  • Loading branch information
lukesthl committed Dec 29, 2023
1 parent 9adb9a0 commit 7ad5db1
Show file tree
Hide file tree
Showing 17 changed files with 323 additions and 232 deletions.
6 changes: 3 additions & 3 deletions apps/expo/app/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,9 @@ function RootLayoutNav() {
useEffect(() => {
const checkShortcut = () => {
void listenForShortcut()
.then(({ app }) => {
console.log("shortcut", app);
router.replace(`/break/${app}`);
.then(({ app, timestamp }) => {
console.log("shortcut", app, timestamp);
router.replace(`/break/${app}?timestamp=${timestamp}`);
})
.catch((error) => {
console.log(JSON.stringify(error));
Expand Down
29 changes: 27 additions & 2 deletions apps/expo/app/break/[appShortcutName].tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useEffect, useRef, useState } from "react";
import { Animated, Easing } from "react-native";
import * as Haptics from "expo-haptics";
import { router, useLocalSearchParams } from "expo-router";
import type { AnimationObject } from "lottie-react-native";
import LottieView from "lottie-react-native";
Expand All @@ -11,14 +12,21 @@ import { BreakStore } from "../../data/break.store";
import { OverviewStore } from "../../data/overview.store";

const AnimatedLottieView = Animated.createAnimatedComponent(LottieView);
const peakProgress = 0.65;
let lastProgress = 0;
let progressStep = 0.05;
const floatingProgress = 0.75;

const Break = observer(() => {
const [loaded, setLoaded] = useState(false);
const [breakStatus, setBreakStatus] = useState<"running" | "finished">("finished");
const searchParams = useLocalSearchParams<{ appShortcutName: string }>();
const searchParams = useLocalSearchParams<{ appShortcutName: string; timestamp: string }>();
useEffect(() => {
if (loaded) return;
void BreakStore.init({ appShortcutName: searchParams.appShortcutName }).then(() => {
void BreakStore.init({
appShortcutName: searchParams.appShortcutName,
timestamp: parseInt(searchParams.timestamp),
}).then(() => {
setLoaded(true);
});
void OverviewStore.init();
Expand All @@ -38,7 +46,23 @@ const Break = observer(() => {
setBreakStatus("running");

animationProgress.current.addListener(({ value }) => {
if (value > lastProgress + progressStep && value < peakProgress) {
lastProgress = value;
void Haptics.impactAsync(
value < peakProgress / 3 ? Haptics.ImpactFeedbackStyle.Light : Haptics.ImpactFeedbackStyle.Medium
);
progressStep -= 0.002;
}
if (value > peakProgress && value > floatingProgress && value > lastProgress + progressStep) {
lastProgress = value;
void Haptics.impactAsync(
value > 0.8 ? Haptics.ImpactFeedbackStyle.Light : Haptics.ImpactFeedbackStyle.Medium
);
progressStep += 0.0015;
}
if (value === 1) {
lastProgress = 0;
progressStep = 0.05;
setBreakStatus("finished");
}
});
Expand Down Expand Up @@ -128,6 +152,7 @@ const Break = observer(() => {
void BreakStore.openApp();
}}
variant="outlined"
color="$text11"
>
{`Open ${selectedApp.name}`}
</Button>
Expand Down
1 change: 0 additions & 1 deletion apps/expo/app/settings/theme.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

import { Check, Moon, Smartphone, Sun } from "@tamagui/lucide-icons";
import { ListItem, View, YGroup, YStack } from "tamagui";

Expand Down
4 changes: 2 additions & 2 deletions apps/expo/components/theme-provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@ export const ThemeProvider = ({ children }: { children: React.ReactNode }) => {
clearTimeout(onColorSchemeChange.current);
}
const storedTheme = await AsyncStorage.getItem("theme");
setAutoTheme(storedTheme === "auto");
if (storedTheme === "auto") {
setAutoTheme(storedTheme === "auto" || !storedTheme);
if (storedTheme === "auto" || !storedTheme) {
setThemeState(colorScheme === "dark" ? "dark" : "light");
} else {
setThemeState(storedTheme as ThemeType);
Expand Down
61 changes: 59 additions & 2 deletions apps/expo/data/break.store.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { Vibration } from "react-native";
import Constants, { AppOwnership } from "expo-constants";
import * as Haptics from "expo-haptics";
import * as Linking from "expo-linking";
import { router } from "expo-router";
import { makeAutoObservable } from "mobx";

import * as ExpoExitApp from "../../../packages/expo-exit-app";
Expand Down Expand Up @@ -35,16 +38,68 @@ export class BreakStoreSingleton {

private appsStore = new AppsStore();

private lastBreakTimestamp = Date.now();

private _app: App | null = null;

constructor() {
makeAutoObservable(this);
}

public async init({ appShortcutName }: { appShortcutName: string }) {
public async init({ appShortcutName, timestamp }: { appShortcutName: string; timestamp: number }) {
const app = await this.appsStore.getOrCreateApp({ appShortcutName });
this.app = app;
void this.appStatisticsStore.trackEvent({ appId: app.id, type: "break-start" });
if (timestamp !== this.lastBreakTimestamp) {
this.lastBreakTimestamp = timestamp;
void this.appStatisticsStore.trackEvent({ appId: app.id, type: "break-start" });
}
// void this.hapticImpact();
}
private getHapticImpactEnum = (impact: string): Haptics.ImpactFeedbackStyle | undefined => {
switch (impact) {
case "heavy":
return Haptics.ImpactFeedbackStyle.Heavy;
case "medium":
return Haptics.ImpactFeedbackStyle.Medium;
case "light":
return Haptics.ImpactFeedbackStyle.Light;
}
};

public async hapticImpact(): Promise<void> {
const pattern: ("light" | "medium" | "heavy" | "vibrate" | number)[] = [];
const duration = this.app?.settings.breakDurationSeconds ?? 0;
const durationInMs = duration * 1;
for (let i = 0; i < durationInMs; ++i) {
if (i < duration / 2) {
pattern.push("light");
} else {
pattern.push("medium");
}
pattern.push(100);
}
console.log("Haptic pattern", pattern);
for (let i = 0; i < pattern.length; ++i) {
const e = pattern[i];
if (i % 2 === 0) {
// Vibration length, always 400 for iOS
if (typeof e === "number") {
Vibration.vibrate(e);
await new Promise((res) => setTimeout(res, e));
// Default
} else if (e === "vibrate" || !e) {
Vibration.vibrate();
// Use native impact type
} else {
console.log("Haptic impact", e);
await Haptics.impactAsync(this.getHapticImpactEnum(e) ?? Haptics.ImpactFeedbackStyle.Medium);
}
// Await for the pause
} else {
if (typeof e !== "number") return;
await new Promise((res) => setTimeout(res, e));
}
}
}

public async openApp(): Promise<void> {
Expand All @@ -53,8 +108,10 @@ export class BreakStoreSingleton {
}
await this.appStatisticsStore.trackEvent({ appId: this.app.id, type: "app-reopen" });
await updateOpenedApp(this.app.key, "app-reopen");
await new Promise((resolve) => setTimeout(resolve, 500));
// await AsyncStorage.setItem("openedApp", `${this.app.key}_${Date.now()}_app-reopen`);
await Linking.openURL(deepLinks[this.app.key as keyof typeof deepLinks]);
router.replace("/");
}

public async exitApp(): Promise<void> {
Expand Down
6 changes: 3 additions & 3 deletions apps/expo/data/shortcut.listener.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@ let intervalId: NodeJS.Timeout | null = null;
const storageKey = "openedApp";
const pathSplitted = FileSystem.documentDirectory?.split("/");
const appPath = pathSplitted?.slice(0, pathSplitted.length - 2).join("/");
const dataPath = `${appPath}/Library/Application Support/${appConfig.bundleIdentifier}/RCTAsyncLocalStorage_V1/manifest.json`;
const dataPath = `${appPath}/Library/Application Support/${appConfig.bundleIdentifier}/RCTAsyncLocalStorage_V1/appintent.json`;

// why FileSystem? because AsyncStorage doesnt work in combination with the App Intent.
// It seems like AsyncStorage caches the value and not directly writes it to the file system.
export const listenForShortcut = async (): Promise<{ app: string }> => {
export const listenForShortcut = async (): Promise<{ app: string; timestamp: number }> => {
let tryCount = 0;
return new Promise((resolve, reject) => {
const time = new Date().getTime();
Expand All @@ -24,7 +24,7 @@ export const listenForShortcut = async (): Promise<{ app: string }> => {
console.log(`took ${new Date().getTime() - time}ms`);
console.log(`openedApp: ${appPayload.app}`);

resolve({ app: appPayload.app });
resolve({ app: appPayload.app, timestamp: parseInt(appPayload.timestamp, 10) });
} else {
throw new Error("no app");
}
Expand Down
2 changes: 1 addition & 1 deletion apps/expo/ios/Podfile
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ elsif podfile_properties.key?('ios.flipper') then
end
end

target 'digitalbreak' do
target 'DigitalBreak' do
use_expo_modules!
config = use_native_modules!

Expand Down
8 changes: 7 additions & 1 deletion apps/expo/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,8 @@ PODS:
- ExpoModulesCore
- ExpoExitApp (0.1.0):
- ExpoModulesCore
- ExpoHaptics (12.4.0):
- ExpoModulesCore
- ExpoHead (0.0.20):
- ExpoModulesCore
- ExpoKeepAwake (12.3.0):
Expand Down Expand Up @@ -584,6 +586,7 @@ DEPENDENCIES:
- ExpoDocumentPicker (from `../../../node_modules/expo-document-picker/ios`)
- ExpoDynamicAppIcon (from `../../../node_modules/expo-dynamic-app-icon/ios`)
- ExpoExitApp (from `../../../packages/expo-exit-app/ios`)
- ExpoHaptics (from `../../../node_modules/expo-haptics/ios`)
- ExpoHead (from `../../../node_modules/expo-head/ios`)
- ExpoKeepAwake (from `../../../node_modules/expo-keep-awake/ios`)
- ExpoLinearGradient (from `../../../node_modules/expo-linear-gradient/ios`)
Expand Down Expand Up @@ -689,6 +692,8 @@ EXTERNAL SOURCES:
:path: "../../../node_modules/expo-dynamic-app-icon/ios"
ExpoExitApp:
:path: "../../../packages/expo-exit-app/ios"
ExpoHaptics:
:path: "../../../node_modules/expo-haptics/ios"
ExpoHead:
:path: "../../../node_modules/expo-head/ios"
ExpoKeepAwake:
Expand Down Expand Up @@ -823,6 +828,7 @@ SPEC CHECKSUMS:
ExpoDocumentPicker: 5cb7389ff935b4addefdd466a606de51a512e922
ExpoDynamicAppIcon: b0f96e53d0bf00412bbe97da0cfc15b8c94a54ce
ExpoExitApp: 5ae87b4d35a0d3b28b5b26bdddcd734e0a41e7f3
ExpoHaptics: 360af6898407ee4e8265d30a1a8fb16491a660eb
ExpoHead: ac95331e2a45cb94f6aee8ba6b8cb3d0a2cc6817
ExpoKeepAwake: be4cbd52d9b177cde0fd66daa1913afa3161fc1d
ExpoLinearGradient: 5d18293f89c063036121281175c4653d6a7c34a2
Expand Down Expand Up @@ -884,6 +890,6 @@ SPEC CHECKSUMS:
SocketRocket: f32cd54efbe0f095c4d7594881e52619cfe80b17
Yoga: 4c3aa327e4a6a23eeacd71f61c81df1bcdf677d5

PODFILE CHECKSUM: 88e1cd7e497076ca7caf6b80c9ced4948b93e976
PODFILE CHECKSUM: 1aa58fa68befae1c918a8ebb53b1c1431742077d

COCOAPODS: 1.12.1
Loading

0 comments on commit 7ad5db1

Please sign in to comment.