Compare commits

..

6 Commits

Author SHA1 Message Date
Ammar Ahmed
00d3f693b0 mobile: fix crash when taking camera picture 2024-05-07 15:53:57 +05:00
Ammar Ahmed
978e63ea78 mobile: fix camera permission 2024-05-07 14:54:19 +05:00
Ammar Ahmed
9d5bec3bfb ci: upload sourcemaps 2024-05-07 11:49:58 +05:00
Abdullah Atta
ac96354b4c core: handle case when updating item while a push is ongoing 2024-05-06 22:04:04 +05:00
Abdullah Atta
63f44d6fbc core: set synced to false on removing content by note id 2024-05-06 15:45:45 +05:00
Abdullah Atta
f9bfa88c83 core: set synced to false on unlinking relations 2024-05-06 15:45:45 +05:00
18 changed files with 163 additions and 63 deletions

View File

@@ -147,8 +147,10 @@ jobs:
apps/mobile/native/android/app/build/outputs/apk/release/notesnook-x86.apk
apps/mobile/native/android/app/build/outputs/apk/release/notesnook-x86_64.apk
- name: Upload signed aab to Github
- name: Upload sourcemaps
uses: actions/upload-artifact@v2
with:
name: Notesnook.aab
path: ${{steps.sign_app.outputs.signedReleaseFile}}
name: sourcemaps
path: |
apps/mobile/native/android/app/build/**/*.map
packages/editor-mobile/sourcemaps/*.map

View File

@@ -133,7 +133,6 @@ jobs:
status: completed
whatsNewDirectory: apps/mobile/native/android/releasenotes/
- name: Create release draft on Github
uses: softprops/action-gh-release@v1
with:
@@ -148,8 +147,10 @@ jobs:
apps/mobile/native/android/app/build/outputs/apk/release/notesnook-x86.apk
apps/mobile/native/android/app/build/outputs/apk/release/notesnook-x86_64.apk
- name: Upload signed aab to Github
- name: Upload sourcemaps
uses: actions/upload-artifact@v2
with:
name: Notesnook.aab
path: ${{steps.sign_app.outputs.signedReleaseFile}}
name: sourcemaps
path: |
apps/mobile/native/android/app/build/**/*.map
packages/editor-mobile/sourcemaps/*.map

View File

@@ -100,5 +100,8 @@ jobs:
- name: Upload Notesnook.ipa to Github
uses: actions/upload-artifact@v2
with:
name: Notesnook.ipa
path: Notesnook.ipa
name: Notesnook.zip
path: |
Notesnook.ipa
apps/mobile/native/ios/**/*.map
packages/editor-mobile/sourcemaps/*.map

View File

@@ -61,7 +61,7 @@ export default function AttachImage({
Attaching {response?.length} image(s):
</Paragraph>
<ScrollView horizontal>
{response?.map((item) => (
{response?.map?.((item) => (
<TouchableOpacity key={item.filename} activeOpacity={0.9}>
<Image
source={{

View File

@@ -186,7 +186,12 @@ const camera = async (options: PickerOptions) => {
maxFiles: 10,
writeTempFile: true
})
.then((response) => handleImageResponse(response, options))
.then((response) => {
handleImageResponse(
Array.isArray(response) ? response : [response],
options
);
})
.catch((e) => {
console.log("camera error: ", e);
});
@@ -211,7 +216,12 @@ const gallery = async (options: PickerOptions) => {
cropping: false,
multiple: true
})
.then((response) => handleImageResponse(response, options))
.then((response) =>
handleImageResponse(
Array.isArray(response) ? response : [response],
options
)
)
.catch((e) => {
console.log("gallery error: ", e);
});

View File

@@ -15,6 +15,8 @@
<uses-permission android:name="android.permission.DOWNLOAD_WITHOUT_NOTIFICATION" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" tools:node="remove"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" tools:node="remove"/>
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES"/>
<uses-permission android:name="android.permission.CAMERA"/>
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<queries>

View File

@@ -95,6 +95,66 @@ test(
TEST_TIMEOUT
);
test(
"edge case 1: items updated while push is running",
async (t) => {
const [deviceA, deviceB] = await Promise.all([
initializeDevice("deviceA"),
initializeDevice("deviceB")
]);
t.onTestFinished(async () => {
console.log(`${t.task.name} log out`);
await cleanup(deviceA, deviceB);
});
const id = await deviceA.notes.add({ title: "hello" });
for (let i = 0; i < 10; ++i) {
await Promise.all([
deviceA.sync({ type: "send" }),
new Promise((resolve) => setTimeout(resolve), 100).then(() =>
deviceA.notes.add({ id, title: `edit ${i}` })
)
]);
expect((await deviceA.notes.note(id))?.synced).toBe(false);
await deviceA.sync({ type: "send" });
await deviceB.sync({ type: "fetch" });
expect((await deviceB.notes.note(id))?.title).toBe(`edit ${i}`);
}
},
TEST_TIMEOUT * 10
);
test(
"edge case 2: new items added while push is running",
async (t) => {
const [deviceA, deviceB] = await Promise.all([
initializeDevice("deviceA"),
initializeDevice("deviceB")
]);
t.onTestFinished(async () => {
console.log(`${t.task.name} log out`);
await cleanup(deviceA, deviceB);
});
for (let i = 0; i < 10; ++i) {
await Promise.all([
deviceA.sync({ type: "send" }),
new Promise((resolve) => setTimeout(resolve), 100).then(() =>
deviceA.notes.add({ title: `note ${i}` })
)
]);
expect(await deviceB.notes.all.count()).toBe(i);
await deviceA.sync({ type: "send" });
await deviceB.sync({ type: "fetch" });
expect(await deviceB.notes.all.count()).toBe(i + 1);
}
},
TEST_TIMEOUT * 10
);
// test(
// "case 4: Device A's sync is interrupted halfway and Device B makes some changes afterwards and syncs.",
// async () => {

View File

@@ -51,7 +51,7 @@ async function writeEncryptedBase64(
hashType,
iv: "some iv",
salt: key.salt!,
length: data.length
size: data.length
};
}

View File

@@ -90,6 +90,25 @@ test("localOnly note should get included as a deleted item in collector", () =>
expect(items[1].type).toBe("note");
}));
test("unlinked relation should get included in collector", () =>
databaseTest().then(async (db) => {
await loginFakeUser(db);
const collector = new Collector(db);
await db.relations.add(
{ id: "h", type: "note" },
{ id: "h", type: "attachment" }
);
await iteratorToArray(collector.collect(100, false));
await db.relations.from({ id: "h", type: "note" }, "attachment").unlink();
const items = await iteratorToArray(collector.collect(100, false));
expect(items).toHaveLength(1);
expect(items[0].items[0].id).toBe("cd93df7a4c64fbd5f100361d629ac5b5");
}));
async function iteratorToArray(iterator) {
let items = [];
for await (const item of iterator) {

View File

@@ -46,6 +46,7 @@ class Collector {
for (const itemType of SYNC_ITEM_TYPES) {
const collectionKey = SYNC_COLLECTIONS_MAP[itemType];
const collection = this.db[collectionKey].collection;
let pushTimestamp = Date.now();
for await (const chunk of collection.unsynced(chunkSize, isForceSync)) {
const items = await this.prepareChunk(chunk, key);
if (!items) continue;
@@ -54,8 +55,20 @@ class Collector {
await collection.update(
chunk.map((i) => i.id),
{ synced: true },
{ sendEvent: false }
{
sendEvent: false,
// EDGE CASE:
// Sometimes an item can get updated while it's being pushed.
// The result is that its `synced` property becomes true even
// though it's modification wasn't yet synced.
// In order to prevent that, we only set the `synced` property
// to true for items that haven't been modified since we last ran
// the push. Everything else will be collected again in the next
// push.
condition: (eb) => eb("dateModified", "<=", pushTimestamp)
}
);
pushTimestamp = Date.now();
}
}
}

View File

@@ -197,7 +197,7 @@ export class Content implements ICollection {
await this.db
.sql()
.replaceInto("content")
.columns(["id", "dateModified", "deleted"])
.columns(["id", "dateModified", "deleted", "synced"])
.expression((eb) =>
eb
.selectFrom("content")
@@ -205,7 +205,8 @@ export class Content implements ICollection {
.select((eb) => [
"content.id",
eb.lit(Date.now()).as("dateModified"),
eb.lit(1).as("deleted")
eb.lit(1).as("deleted"),
eb.lit(0).as("synced")
])
)
.execute();

View File

@@ -154,7 +154,7 @@ export class Relations implements ICollection {
await this.db
.sql()
.replaceInto("relations")
.columns(["id", "dateModified", "deleted"])
.columns(["id", "dateModified", "deleted", "synced"])
.expression((eb) =>
eb
.selectFrom("relations")
@@ -169,7 +169,8 @@ export class Relations implements ICollection {
.select((eb) => [
"relations.id",
eb.lit(Date.now()).as("dateModified"),
eb.lit(1).as("deleted")
eb.lit(1).as("deleted"),
eb.lit(0).as("synced")
])
)
.execute();
@@ -265,7 +266,7 @@ class RelationsArray<TType extends keyof RelatableTable> {
await this.db
.sql()
.replaceInto("relations")
.columns(["id", "dateModified", "deleted"])
.columns(["id", "dateModified", "deleted", "synced"])
.expression((eb) =>
eb
.selectFrom("relations")
@@ -274,7 +275,8 @@ class RelationsArray<TType extends keyof RelatableTable> {
.select((eb) => [
"relations.id",
eb.lit(Date.now()).as("dateModified"),
eb.lit(1).as("deleted")
eb.lit(true).as("deleted"),
eb.lit(false).as("synced")
])
)
.execute();

View File

@@ -226,7 +226,14 @@ export class SQLCollection<
async update(
ids: string[],
partial: Partial<SQLiteItem<T>>,
options: { sendEvent: boolean } = { sendEvent: true }
options: {
sendEvent: boolean;
condition?: ExpressionOrFactory<
DatabaseSchema,
keyof DatabaseSchema,
SqlBool
>;
} = { sendEvent: true }
) {
if (!this.sanitizer.sanitize(this.type, partial)) return;
@@ -237,6 +244,7 @@ export class SQLCollection<
await tx
.updateTable<keyof DatabaseSchema>(this.type)
.where("id", "in", chunk)
.$if(!!options.condition, (eb) => eb.where(options.condition!))
.set({
...partial,
dateModified: Date.now(),

View File

@@ -1,7 +1,7 @@
module.exports = {
all: {
DISABLE_ESLINT_PLUGIN: true,
GENERATE_SOURCEMAP: process.env.NODE_ENV === "development",
GENERATE_SOURCEMAP: true,
BROWSER: "none"
}
};

View File

@@ -27,7 +27,7 @@
},
"scripts": {
"start": "env-cmd -e all react-scripts start",
"build": "env-cmd -e all react-scripts build && rm -rf build.bundle && mv build build.bundle",
"build": "env-cmd -e all react-scripts build && rm -rf build.bundle && rm -rf sourcemaps && mv build build.bundle && cp -r ./build.bundle/static/js/ sourcemaps && rm -r ./build.bundle/static/js/*.map",
"test": "react-scripts test",
"eject": "react-scripts eject",
"postinstall": "patch-package"

View File

@@ -20,6 +20,10 @@
/* color: var(--nn_primary_paragraph) ## TODO: use fixed color */
}
.main-editor > .ProseMirror:first-child {
margin-top: -5px !important;
}
::selection {
color: white;
background-color: var(--nn_primary_accent);

View File

@@ -196,14 +196,7 @@ const Tiptap = ({
? useTabStore.getState().noteState[tabRef.current.noteId]
: undefined;
const top = noteState?.top;
logger(
"info",
"editor.onCreate",
tabRef.current?.noteId,
noteState?.top,
noteState?.to,
noteState?.from
);
logger("info", tabRef.current.noteId, noteState?.top);
if (noteState?.to || noteState?.from) {
editors[tabRef.current.id]?.chain().setTextSelection({
@@ -211,7 +204,6 @@ const Tiptap = ({
from: noteState.from
});
}
logger("info", "Scrolling to", top, useTabStore.getState().noteState);
containerRef.current?.scrollTo({
left: 0,
top: top || 0,
@@ -242,6 +234,7 @@ const Tiptap = ({
const _editor = useTiptap(tiptapOptions, [tiptapOptions]);
const update = useCallback(() => {
logger("info", "LOADING NOTE...");
editors[tabRef.current.id]?.commands.setTextSelection(0);
setTick((tick) => tick + 1);
globalThis.editorControllers[tabRef.current.id]?.setTitlePlaceholder(
@@ -285,13 +278,6 @@ const Tiptap = ({
? state.noteState[tabRef.current.noteId]
: undefined;
if (noteState) {
if (
containerRef.current &&
containerRef.current?.scrollHeight < noteState.top
) {
console.log("Container too small to scroll.");
return;
}
containerRef.current?.scrollTo({
left: 0,
top: noteState.top,
@@ -331,7 +317,6 @@ const Tiptap = ({
};
updateScrollPosition(useTabStore.getState());
const unsub = useTabStore.subscribe((state, prevState) => {
if (state.currentTab !== tabRef.current.id) {
isFocusedRef.current = false;

View File

@@ -54,7 +54,6 @@ type Timers = {
selectionChange: NodeJS.Timeout | null;
change: NodeJS.Timeout | null;
wordCounter: NodeJS.Timeout | null;
scroll: NodeJS.Timeout | null;
};
function isInViewport(element: any) {
@@ -136,8 +135,7 @@ export function useEditorController({
const timers = useRef<Timers>({
selectionChange: null,
change: null,
wordCounter: null,
scroll: null
wordCounter: null
});
if (!tabRef.current.noteId && loading) {
@@ -212,19 +210,14 @@ export function useEditorController({
const scroll = useCallback(
(_event: React.UIEvent<HTMLDivElement, UIEvent>) => {
const value = _event.currentTarget.scrollTop;
if (timers.current.scroll !== null) clearTimeout(timers.current.scroll);
timers.current.scroll = setTimeout(() => {
if (
tabRef.current.noteId &&
tabRef.current.noteId === useTabStore.getState().getCurrentNoteId()
) {
logger("info", tabRef.current.noteId, value);
useTabStore.getState().setNoteState(tabRef.current.noteId, {
top: value
});
}
}, 16);
const noteId = useTabStore
.getState()
.getNoteIdForTab(useTabStore.getState().currentTab);
if (noteId) {
useTabStore.getState().setNoteState(noteId, {
top: _event.currentTarget.scrollTop
});
}
},
[]
);
@@ -266,13 +259,10 @@ export function useEditorController({
preserveWhitespace: true
});
if (editor.isFocused && (to !== 1 || from !== 1)) {
logger("info", "Setting focus", to, from);
editor.commands.setTextSelection({
from,
to
});
}
editor.commands.setTextSelection({
from,
to
});
countWords(0);
}