mirror of
https://github.com/streetwriters/notesnook.git
synced 2026-08-30 18:48:27 +02:00
Compare commits
6 Commits
fix-editor
...
fix-camera
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
00d3f693b0 | ||
|
|
978e63ea78 | ||
|
|
9d5bec3bfb | ||
|
|
ac96354b4c | ||
|
|
63f44d6fbc | ||
|
|
f9bfa88c83 |
@@ -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
|
||||
|
||||
9
.github/workflows/android.publish.yml
vendored
9
.github/workflows/android.publish.yml
vendored
@@ -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
|
||||
|
||||
7
.github/workflows/ios.publish.yml
vendored
7
.github/workflows/ios.publish.yml
vendored
@@ -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
|
||||
|
||||
@@ -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={{
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -51,7 +51,7 @@ async function writeEncryptedBase64(
|
||||
hashType,
|
||||
iv: "some iv",
|
||||
salt: key.salt!,
|
||||
length: data.length
|
||||
size: data.length
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
module.exports = {
|
||||
all: {
|
||||
DISABLE_ESLINT_PLUGIN: true,
|
||||
GENERATE_SOURCEMAP: process.env.NODE_ENV === "development",
|
||||
GENERATE_SOURCEMAP: true,
|
||||
BROWSER: "none"
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user