mobile: migrate share to ts

This commit is contained in:
Ammar Ahmed
2025-10-22 11:11:10 +05:00
parent d468a8274e
commit c189c63c3d
7 changed files with 209 additions and 131 deletions

View File

@@ -16,16 +16,22 @@ GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { Note, NoteContent } from "@notesnook/core";
import { useThemeColors } from "@notesnook/theme";
import React, {
RefObject,
useCallback,
useEffect,
useLayoutEffect,
useRef,
useState
} from "react";
import { Linking, Platform, TextInput, View } from "react-native";
import { Linking, Platform, TextInput, View, ViewStyle } from "react-native";
import { WebView } from "react-native-webview";
import {
ShouldStartLoadRequest,
WebViewMessageEvent
} from "react-native-webview/lib/WebViewTypes";
import { eSubscribeEvent, eUnSubscribeEvent } from "../services/event-manager";
import { eOnLoadNote } from "../utils/events";
import { defaultBorderRadius } from "../utils/size";
@@ -42,7 +48,11 @@ export const EDITOR_URI = __DEV__
? EditorMobileSourceUrl
: EditorMobileSourceUrl;
export async function post(ref, type, value = null) {
export async function post(
ref: RefObject<any>,
type: string,
value: any = null
) {
const message = {
type,
value
@@ -51,17 +61,25 @@ export async function post(ref, type, value = null) {
}
const useEditor = () => {
const ref = useRef();
const ref = useRef<WebView>(null);
const { colors } = useThemeColors("editor");
const currentNote = useRef();
const currentNote = useRef<
Note & {
content: NoteContent<false>;
}
>(undefined);
const postMessage = useCallback(
async (type, data) => post(ref, type, data),
async (type: string, data: any) => post(ref, type, data),
[]
);
const loadNote = useCallback(
(note) => {
(
note: Note & {
content: NoteContent<false>;
}
) => {
postMessage("html", note.content.data);
currentNote.current = note;
},
@@ -104,8 +122,11 @@ const useEditor = () => {
return { ref, onLoad, currentNote };
};
const useEditorEvents = (editor, onChange) => {
const onMessage = (event) => {
const useEditorEvents = (
editor: ReturnType<typeof useEditor>,
onChange: (data: string) => void
) => {
const onMessage = (event: WebViewMessageEvent) => {
const data = event.nativeEvent.data;
const editorMessage = JSON.parse(data);
@@ -118,7 +139,7 @@ const useEditorEvents = (editor, onChange) => {
return onMessage;
};
const onShouldStartLoadWithRequest = (request) => {
const onShouldStartLoadWithRequest = (request: ShouldStartLoadRequest) => {
if (request.url.includes("https")) {
if (Platform.OS === "ios" && !request.isTopFrame) return true;
Linking.openURL(request.url);
@@ -128,7 +149,7 @@ const onShouldStartLoadWithRequest = (request) => {
}
};
const style = {
const style: ViewStyle = {
height: "100%",
maxHeight: "100%",
width: "100%",
@@ -136,32 +157,46 @@ const style = {
backgroundColor: "transparent"
};
export const Editor = ({ onChange, onLoad, editorRef }) => {
export type EditorRef = {
focus: () => void;
};
export const Editor = ({
onChange,
onLoad,
editorRef
}: {
onChange: (data: string) => void;
onLoad: () => void;
editorRef: RefObject<EditorRef | null>;
}) => {
const { colors } = useThemeColors();
const editor = useEditor();
const inputRef = useRef();
const inputRef = useRef<TextInput>(null);
const onMessage = useEditorEvents(editor, onChange);
const [loading, setLoading] = useState(true);
useLayoutEffect(() => {
onLoad?.();
}, [onLoad]);
if (editorRef) {
editorRef.current = {
focus: () => {
setTimeout(() => {
inputRef.current?.focus();
editor.ref.current?.injectJavaScript(`(() => {
useEffect(() => {
if (editorRef) {
editorRef.current = {
focus: () => {
setTimeout(() => {
inputRef.current?.focus();
editor.ref.current?.injectJavaScript(`(() => {
const editor = document.getElementById('editor');
if (editor) {
editor.focus();
}
})();`);
editor.ref?.current?.requestFocus();
});
}
};
}
editor.ref?.current?.requestFocus();
});
}
};
}
}, []);
return (
<View

View File

@@ -27,18 +27,20 @@ import { Platform } from "react-native";
import RNFetchBlob from "react-native-blob-util";
import WebView from "react-native-webview";
import { Config } from "./store";
import { db } from "../common/database";
import { SUBSCRIPTION_STATUS } from "../utils/constants";
export const fetchHandle = createRef();
export const fetchHandle = createRef<{
processUrl: (url: string) => Promise<string | null>;
}>();
export const HtmlLoadingWebViewAgent = React.memo(
() => {
const [source, setSource] = useState(null);
const [clipper, setClipper] = useState(null);
const loadHandler = useRef();
const htmlHandler = useRef();
const webview = useRef();
const premium = useRef(false);
const [source, setSource] = useState<string | null>(null);
const [clipper, setClipper] = useState<string | null>(null);
const loadHandler = useRef<((result?: boolean | null) => void) | null>(
null
);
const htmlHandler = useRef<((html: string | null) => void) | null>(null);
const webview = useRef<any>(null);
const corsProxy = Config.corsProxy;
useImperativeHandle(
@@ -71,12 +73,6 @@ export const HtmlLoadingWebViewAgent = React.memo(
useEffect(() => {
(async () => {
const user = await db.user.getUser();
const subscriptionStatus =
user?.subscription?.type || SUBSCRIPTION_STATUS.BASIC;
premium.current =
user && subscriptionStatus !== SUBSCRIPTION_STATUS.BASIC;
const clipperPath =
Platform.OS === "ios"
? RNFetchBlob.fs.dirs.MainBundleDir +
@@ -107,7 +103,7 @@ export const HtmlLoadingWebViewAgent = React.memo(
}}
useSharedProcessPool={false}
pointerEvents="none"
onMessage={(event) => {
onMessage={(event: any) => {
try {
const data = JSON.parse(event.nativeEvent.data);
if (data && data.type === "html") {
@@ -123,7 +119,7 @@ export const HtmlLoadingWebViewAgent = React.memo(
console.log("Error handling webview message", e);
}
}}
injectedJavaScriptBeforeContentLoaded={script(clipper, premium.current)}
injectedJavaScriptBeforeContentLoaded={script(clipper!, corsProxy)}
onError={() => {
console.log("Error loading page");
loadHandler.current?.();
@@ -137,7 +133,7 @@ export const HtmlLoadingWebViewAgent = React.memo(
() => true
);
const script = (clipper, pro) => `
const script = (clipper: string, corsProxy?: string): string => `
${clipper}
function postMessage(type, value) {
@@ -158,10 +154,10 @@ function postMessage(type, value) {
postMessage("error", globalThis.Clipper.clipPage);
} else {
globalThis.Clipper.clipPage(document,false, {
images: ${pro},
images: true,
inlineImages: false,
styles: false,
corsProxy: undefined
corsProxy: ${corsProxy ? `"${corsProxy}"` : `undefined`}
}).then(result => {
postMessage("html", result);
}).catch(e => {

View File

@@ -18,19 +18,18 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { ScopedThemeProvider } from "@notesnook/theme";
import React, { Fragment, useEffect, useState } from "react";
import { Modal, Platform } from "react-native";
import React, { useEffect, useState } from "react";
import { Modal, ModalProps, Platform } from "react-native";
import ShareView from "./share";
import "./store";
const Wrapper = Platform.OS === "android" ? Modal : Fragment;
const outerProps =
Platform.OS === "android"
? {
? ({
animationType: "fade",
transparent: true,
visible: true
}
} as ModalProps)
: {};
const NotesnookShare = ({ quicknote = false }) => {
@@ -42,10 +41,12 @@ const NotesnookShare = ({ quicknote = false }) => {
}, []);
return (
<ScopedThemeProvider value="base">
{!render ? null : (
<Wrapper {...outerProps}>
<ShareView quicknote={quicknote} />
</Wrapper>
{!render ? null : Platform.OS === "android" ? (
<Modal {...outerProps}>
<ShareView />
</Modal>
) : (
<ShareView />
)}
</ScopedThemeProvider>
);

View File

@@ -17,7 +17,9 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import ShareExtension from "@ammarahmed/react-native-share-extension";
import ShareExtension, {
ShareItem
} from "@ammarahmed/react-native-share-extension";
import { getPreviewData } from "@flyerhq/react-native-link-preview";
import {
formatBytes,
@@ -33,6 +35,7 @@ import {
Dimensions,
Image,
Keyboard,
KeyboardEvent,
Platform,
SafeAreaView,
ScrollView,
@@ -53,21 +56,21 @@ import Heading from "../components/ui/typography/heading";
import Paragraph from "../components/ui/typography/paragraph";
import { useDBItem } from "../hooks/use-db-item";
import { eSendEvent } from "../services/event-manager";
import { FILE_SIZE_LIMIT, IMAGE_SIZE_LIMIT } from "../utils/constants";
import { eOnLoadNote } from "../utils/events";
import { NoteBundle } from "../utils/note-bundle";
import { defaultBorderRadius, AppFontSize } from "../utils/size";
import { AddNotebooks } from "./add-notebooks";
import { AddTags } from "./add-tags";
import { Editor } from "./editor";
import { Editor, EditorRef } from "./editor";
import { HtmlLoadingWebViewAgent, fetchHandle } from "./fetch-webview";
import { Search } from "./search";
import { initDatabase, useShareStore } from "./store";
import { isTablet } from "react-native-device-info";
const getLinkPreview = (url) => {
const getLinkPreview = (url: string) => {
return getPreviewData(url, 5000);
};
async function sanitizeHtml(site) {
async function sanitizeHtml(site: string) {
try {
let html = await fetchHandle.current?.processUrl(site);
return html;
@@ -76,11 +79,11 @@ async function sanitizeHtml(site) {
}
}
function makeHtmlFromUrl(url) {
function makeHtmlFromUrl(url: string) {
return `<a href='${url}' target='_blank'>${url}</a>`;
}
function makeHtmlFromPlainText(text) {
function makeHtmlFromPlainText(text: string) {
if (!text) return "";
return `<p>${text
@@ -88,12 +91,22 @@ function makeHtmlFromPlainText(text) {
.replace(/(?:\r\n|\r|\n)/g, "</p><p>")}</p>`;
}
let defaultNote = {
title: null,
id: null,
type DefaultNote = {
title?: string;
id?: string;
sessionId?: number;
content: {
type: "tiptap";
data?: string;
};
};
let defaultNote: DefaultNote = {
title: "",
id: undefined,
content: {
type: "tiptap",
data: null
data: ""
}
};
@@ -115,32 +128,39 @@ const modes = {
}
};
declare global {
var IS_SHARE_EXTENSION: boolean;
var IS_MAIN_APP_RUNNING: boolean;
}
const ShareView = () => {
const { colors } = useThemeColors();
const appendNoteId = useShareStore((state) => state.appendNote);
const [note, setNote] = useState({ ...defaultNote });
const noteContent = useRef("");
const noteTitle = useRef("");
const noteContent = useRef<string>(undefined);
const noteTitle = useRef<string>(undefined);
const [loading, setLoading] = useState(false);
const [loadingExtension, setLoadingExtension] = useState(true);
const fullQualityImages = useIsFeatureAvailable("fullQualityImages");
const [rawData, setRawData] = useState({
type: null,
value: null
});
const inputRef = useRef(null);
const [rawData, setRawData] = useState<{
type?: string;
value?: string;
}>({});
const inputRef = useRef<TextInput>(null);
const [mode, setMode] = useState(1);
const keyboardHeight = useRef(0);
const { width, height } = useWindowDimensions();
const [loadingPage, setLoadingPage] = useState(false);
const editorRef = useRef();
const [searchMode, setSearchMode] = useState(null);
const [rawFiles, setRawFiles] = useState([]);
const editorRef = useRef<EditorRef>(null);
const [searchMode, setSearchMode] = useState<
"appendNote" | "selectTags" | "selectNotebooks" | null
>(null);
const [rawFiles, setRawFiles] = useState<ShareItem[]>([]);
const [kh, setKh] = useState(0);
const [compress, setCompress] = useState(true);
globalThis["IS_SHARE_EXTENSION"] = true;
const onKeyboardDidShow = (event) => {
const onKeyboardDidShow = (event: KeyboardEvent) => {
let height = Dimensions.get("screen").height - event.endCoordinates.screenY;
keyboardHeight.current = height;
setKh(height);
@@ -172,7 +192,7 @@ const ShareView = () => {
}
}, [fullQualityImages]);
const showLinkPreview = async (note, link) => {
const showLinkPreview = async (note: DefaultNote, link: string) => {
let _note = note;
_note.content.data = makeHtmlFromUrl(link);
try {
@@ -184,20 +204,33 @@ const ShareView = () => {
return note;
};
const onLoad = useCallback(() => {
console.log(noteContent.current, "current...");
eSendEvent(eOnLoadNote + "shareEditor", {
id: null,
content: {
type: "tiptap",
data: noteContent.current
},
forced: true
});
}, []);
const loadData = useCallback(
async (isEditor) => {
async (isEditor: boolean) => {
try {
if (noteContent.current) {
onLoad();
return;
}
defaultNote.content.data = null;
defaultNote.content.data = undefined;
setNote({ ...defaultNote });
const data = await ShareExtension.data();
if (!data || data.length === 0) {
setRawData({
value: ""
value: "",
type: "text"
});
if (isEditor) {
setTimeout(() => {
@@ -257,7 +290,6 @@ const ShareView = () => {
}
}
onLoad();
setNote({ ...note });
} catch (e) {
console.error(e);
@@ -266,24 +298,12 @@ const ShareView = () => {
[onLoad]
);
const onLoad = useCallback(() => {
console.log(noteContent.current, "current...");
eSendEvent(eOnLoadNote + "shareEditor", {
id: null,
content: {
type: "tiptap",
data: noteContent.current
},
forced: true
});
}, []);
useEffect(() => {
(async () => {
try {
await initDatabase();
setLoadingExtension(false);
loadData();
loadData(false);
useShareStore.getState().restore();
} catch (e) {
DatabaseLogger.error(e);
@@ -306,22 +326,23 @@ const ShareView = () => {
let noteData;
if (appendNoteId) {
if (!(await db.notes.exists(appendNoteId))) {
const note = await db.notes.note(appendNoteId);
if (!note) {
useShareStore.getState().setAppendNote(null);
Alert.alert("The note you are trying to append to has been deleted.");
setLoading(false);
return;
}
const note = await db.notes.note(appendNoteId);
let rawContent = await db.content.get(note.contentId);
let rawContent = note.contentId
? await db.content.get(note.contentId)
: null;
noteData = {
content: {
data: (rawContent?.data || "") + "<br/>" + noteContent.current,
type: "tiptap"
},
id: note.id,
id: note?.id,
sessionId: Date.now()
};
} else {
@@ -354,28 +375,30 @@ const ShareView = () => {
setLoading(false);
};
const changeMode = async (m) => {
setMode(m);
const changeMode = async (value: number) => {
setMode(value);
setLoading(true);
try {
if (m === 2) {
if (value === 2) {
setLoadingPage(true);
setTimeout(async () => {
let html = await sanitizeHtml(rawData.value);
noteContent.current = html;
let html = await sanitizeHtml(rawData?.value || "");
noteContent.current = html || "";
setLoadingPage(false);
onLoad();
setNote((note) => {
note.content.data = html;
note.content.data = html || "";
return { ...note };
});
}, 300);
} else {
setLoadingPage(false);
let html = isURL(rawData.value)
? makeHtmlFromUrl(rawData.value)
: makeHtmlFromPlainText(rawData.value);
let html = !rawData.value
? ""
: isURL(rawData?.value)
? makeHtmlFromUrl(rawData?.value)
: makeHtmlFromPlainText(rawData?.value);
setNote((note) => {
note.content.data = html;
noteContent.current = html;
@@ -395,7 +418,7 @@ const ShareView = () => {
loadData(true);
}, [loadData]);
const onRemoveFile = (item) => {
const onRemoveFile = (item: ShareItem) => {
const index = rawFiles.findIndex((file) => file.name === item.name);
if (index > -1) {
setRawFiles((state) => {
@@ -529,7 +552,7 @@ const ShareView = () => {
defaultValue={noteTitle.current}
blurOnSubmit={false}
onSubmitEditing={() => {
editorRef.current.focus();
editorRef.current?.focus();
}}
/>
)}
@@ -584,9 +607,6 @@ const ShareView = () => {
<TouchableOpacity
activeOpacity={0.9}
key={item.name}
source={{
uri: `file://${item.value}`
}}
onPress={() => onRemoveFile(item)}
style={{
borderRadius: defaultBorderRadius,
@@ -599,7 +619,6 @@ const ShareView = () => {
paddingHorizontal: 8,
marginRight: 6
}}
resizeMode="cover"
>
<Icon
color={colors.primary.icon}
@@ -838,7 +857,7 @@ const ShareView = () => {
<View
style={{
height: Platform.isPad ? 150 : Platform.OS === "ios" ? 110 : 0
height: isTablet() ? 150 : Platform.OS === "ios" ? 110 : 0
}}
/>
</View>
@@ -848,7 +867,13 @@ const ShareView = () => {
);
};
const AppendNote = ({ id, onLoad }) => {
const AppendNote = ({
id,
onLoad
}: {
id: string;
onLoad: (title: string) => void;
}) => {
const { colors } = useThemeColors();
const [item] = useDBItem(id, "note");

View File

@@ -17,11 +17,17 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { ThemeDark, ThemeLight, useThemeEngineStore } from "@notesnook/theme";
import {
ThemeDark,
ThemeDefinition,
ThemeLight,
useThemeEngineStore
} from "@notesnook/theme";
import { Appearance } from "react-native";
import { create } from "zustand";
import { db, setupDatabase } from "../common/database";
import { MMKV } from "../common/database/mmkv";
import { SettingStore } from "../stores/use-setting-store";
export async function initDatabase() {
if (!db.isInitialized) {
@@ -37,9 +43,10 @@ const StorageKeys = {
appSettings: "appSettings"
};
let appSettings = MMKV.getString(StorageKeys.appSettings);
if (appSettings) {
appSettings = JSON.parse(appSettings);
let appSettingsJson = MMKV.getString(StorageKeys.appSettings);
let appSettings: SettingStore["settings"] | null = null;
if (appSettingsJson) {
appSettings = JSON.parse(appSettingsJson) as SettingStore["settings"];
}
const systemColorScheme = Appearance.getColorScheme();
@@ -50,34 +57,48 @@ const currentColorScheme = useSystemTheme ? systemColorScheme : appColorScheme;
const theme =
currentColorScheme === "dark"
? appSettings?.darkTheme
: appSettings?.lightTheme;
: appSettings?.lighTheme;
const currentTheme =
theme || (currentColorScheme === "dark" ? ThemeDark : ThemeLight);
useThemeEngineStore.getState().setTheme(currentTheme);
export const useShareStore = create((set) => ({
export type ShareStore = {
theme: ThemeDefinition;
appendNote: string | null;
selectedTags: string[];
selectedNotebooks: string[];
setAppendNote: (noteId: string | null) => void;
restore: () => void;
setSelectedNotebooks: (selectedNotebooks: string[]) => void;
setSelectedTags: (selectedTags: string[]) => void;
};
export const useShareStore = create<ShareStore>((set) => ({
theme: currentTheme,
appendNote: null,
setAppendNote: (noteId) => {
MMKV.setItem(StorageKeys.appendNote, noteId);
if (!noteId) {
MMKV.removeItem(StorageKeys.appendNote);
} else {
MMKV.setItem(StorageKeys.appendNote, noteId);
}
set({ appendNote: noteId });
},
restore: () => {
let appendNote = MMKV.getString(StorageKeys.appendNote);
let selectedNotebooks = MMKV.getString(StorageKeys.selectedNotebooks);
let selectedTags = MMKV.getString(StorageKeys.selectedTag);
appendNote = JSON.parse(appendNote);
appendNote = appendNote;
set({
appendNote: appendNote,
selectedNotebooks: selectedNotebooks ? JSON.parse(selectedNotebooks) : [],
selectedTag: selectedTags ? JSON.parse(selectedTags) : []
selectedTags: selectedTags ? JSON.parse(selectedTags) : []
});
},
selectedTags: [],
selectedNotebooks: [],
setSelectedNotebooks: (selectedNotebooks) => {
setSelectedNotebooks: (selectedNotebooks: string[]) => {
MMKV.setItem(
StorageKeys.selectedNotebooks,
JSON.stringify(selectedNotebooks)

View File

@@ -14,7 +14,7 @@
"@ammarahmed/react-native-background-fetch": "^4.2.2",
"@ammarahmed/react-native-eventsource": "1.1.0",
"@ammarahmed/react-native-fingerprint-scanner": "^5.0.1",
"@ammarahmed/react-native-share-extension": "^2.9.1",
"@ammarahmed/react-native-share-extension": "^2.9.3",
"@ammarahmed/react-native-sodium": "^1.6.8",
"@azure/core-asynciterator-polyfill": "^1.0.2",
"@bam.tech/react-native-image-resizer": "3.0.11",
@@ -518,9 +518,9 @@
}
},
"node_modules/@ammarahmed/react-native-share-extension": {
"version": "2.9.1",
"resolved": "https://registry.npmjs.org/@ammarahmed/react-native-share-extension/-/react-native-share-extension-2.9.1.tgz",
"integrity": "sha512-9ke3x9orQYb/3h13Cuk21T5YVM1cyLwDLndF22o7/ugFGAVA8VozpoFjHKlr3A6gKnp4m43zEkYfegHvUBj/gQ==",
"version": "2.9.3",
"resolved": "https://registry.npmjs.org/@ammarahmed/react-native-share-extension/-/react-native-share-extension-2.9.3.tgz",
"integrity": "sha512-T7PgFydfcuOWjslTtM7qJMeI9vfQEiJSxavfISRdnguHlvxO1+sttAft19srpGAlw6HH3XXe6D5iioJ2xr7lkA==",
"license": "MIT",
"dependencies": {
"react-native": "^0.63.1"

View File

@@ -30,7 +30,7 @@
"@ammarahmed/react-native-background-fetch": "^4.2.2",
"@ammarahmed/react-native-eventsource": "1.1.0",
"@ammarahmed/react-native-fingerprint-scanner": "^5.0.1",
"@ammarahmed/react-native-share-extension": "^2.9.1",
"@ammarahmed/react-native-share-extension": "^2.9.3",
"@ammarahmed/react-native-sodium": "^1.6.8",
"@azure/core-asynciterator-polyfill": "^1.0.2",
"@bam.tech/react-native-image-resizer": "3.0.11",
@@ -85,6 +85,7 @@
"phone": "^3.1.14",
"qclone": "^1.2.0",
"react": "19.1.1",
"react-async-hook": "^4.0.0",
"react-native": "0.82.0",
"react-native-actions-sheet": "0.9.7",
"react-native-actions-shortcuts": "^1.0.1",
@@ -145,8 +146,7 @@
"toggle-switch-react-native": "3.2.0",
"url": "^0.11.0",
"validator": "^13.5.2",
"zustand": "^4.5.5",
"react-async-hook": "^4.0.0"
"zustand": "^4.5.5"
},
"devDependencies": {
"@babel/core": "^7.27.1",