diff --git a/apps/mobile/android/app/build.gradle b/apps/mobile/android/app/build.gradle index 7cc709a78..1f4781422 100644 --- a/apps/mobile/android/app/build.gradle +++ b/apps/mobile/android/app/build.gradle @@ -233,6 +233,9 @@ dependencies { // The version of react-native is set by the React Native Gradle Plugin implementation("com.facebook.react:react-android") implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.0.0") + // Lets the widgets push their list data straight into the RemoteViews on every API level, + // instead of the deprecated RemoteViewsService adapter (which needs API 31 to do natively). + implementation("androidx.core:core-remoteviews:1.0.0") implementation("androidx.core:core-splashscreen:1.0.0") implementation 'androidx.multidex:multidex:2.0.1' diff --git a/apps/mobile/android/app/src/main/AndroidManifest.xml b/apps/mobile/android/app/src/main/AndroidManifest.xml index 4f46df45c..a3267c669 100644 --- a/apps/mobile/android/app/src/main/AndroidManifest.xml +++ b/apps/mobile/android/app/src/main/AndroidManifest.xml @@ -86,8 +86,9 @@ android:name=".NotePreviewWidget" android:exported="false" android:label="@string/note"> - " + + + + + + + + + - - = MIN_HEIGHT_FOR_BODY_DP; + } + @Override public void onAppWidgetOptionsChanged(Context context, AppWidgetManager appWidgetManager, int appWidgetId, Bundle newOptions) { super.onAppWidgetOptionsChanged(context, appWidgetManager, appWidgetId, newOptions); + // This used to do nothing at all, so resizing the widget left it rendered for its old size. + updateAppWidget(context, appWidgetManager, appWidgetId); } - private static Bundle getActivityOptionsBundle() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { - ActivityOptions activityOptions = ActivityOptions.makeBasic(); - activityOptions.setPendingIntentCreatorBackgroundActivityStartMode( - ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED); - return activityOptions.toBundle(); - } else - return null; + /** + * The note shown by each widget is stored in the "appPreview" preferences under its widget id. + * When the system restores our widgets it hands out fresh ids, so unless we move the stored + * notes over to the new ids the widgets are left permanently blank with no way to recover + * other than removing and re-adding them. + * + * AppWidgetProvider calls onUpdate() with the new ids right after this, which re-renders them. + */ + @Override + public void onRestored(Context context, int[] oldWidgetIds, int[] newWidgetIds) { + super.onRestored(context, oldWidgetIds, newWidgetIds); + if (oldWidgetIds == null || newWidgetIds == null) return; + + int count = Math.min(oldWidgetIds.length, newWidgetIds.length); + SharedPreferences preferences = context.getSharedPreferences("appPreview", Context.MODE_PRIVATE); + + // Read everything up front: an old id can collide with the new id of another widget. + String[] notes = new String[count]; + Set newKeys = new HashSet<>(); + for (int i = 0; i < count; i++) { + notes[i] = preferences.getString(String.valueOf(oldWidgetIds[i]), ""); + newKeys.add(String.valueOf(newWidgetIds[i])); + } + + SharedPreferences.Editor edit = preferences.edit(); + for (int i = 0; i < count; i++) { + String oldKey = String.valueOf(oldWidgetIds[i]); + if (!newKeys.contains(oldKey)) { + edit.remove(oldKey); + } + } + for (int i = 0; i < count; i++) { + if (notes[i].isEmpty()) continue; + edit.putString(String.valueOf(newWidgetIds[i]), notes[i]); + } + edit.apply(); } @Override diff --git a/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/NoteWidget.java b/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/NoteWidget.java index 6207f5ef1..6efe9ed1e 100644 --- a/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/NoteWidget.java +++ b/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/NoteWidget.java @@ -1,12 +1,10 @@ package com.streetwriters.notesnook; -import android.app.ActivityOptions; import android.app.PendingIntent; import android.appwidget.AppWidgetManager; import android.appwidget.AppWidgetProvider; import android.content.Context; import android.content.Intent; -import android.os.Build; import android.os.Bundle; import android.widget.RemoteViews; @@ -24,20 +22,10 @@ public class NoteWidget extends AppWidgetProvider { static void setClickIntent(Context context, RemoteViews views) { Intent intent = new Intent(context, ShareActivity.class); - PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE, getActivityOptionsBundle()); + PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE, WidgetUtils.getActivityOptionsBundle()); views.setOnClickPendingIntent(R.id.new_note, pendingIntent); } - private static Bundle getActivityOptionsBundle() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { - ActivityOptions activityOptions = ActivityOptions.makeBasic(); - activityOptions.setPendingIntentCreatorBackgroundActivityStartMode( - ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED); - return activityOptions.toBundle(); - } else - return null; - } - @Override public void onAppWidgetOptionsChanged(Context context, AppWidgetManager appWidgetManager, int appWidgetId, Bundle newOptions) { super.onAppWidgetOptionsChanged(context, appWidgetManager, appWidgetId, newOptions); diff --git a/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/RCTNNativeModule.java b/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/RCTNNativeModule.java index 806630b69..ac04cebcb 100644 --- a/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/RCTNNativeModule.java +++ b/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/RCTNNativeModule.java @@ -12,7 +12,6 @@ import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; -import android.graphics.RectF; import android.graphics.drawable.Icon; import android.os.Build; import android.os.Bundle; @@ -29,7 +28,6 @@ import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.ReactMethod; import com.facebook.react.bridge.WritableArray; import com.facebook.react.bridge.WritableMap; -import com.google.gson.Gson; import com.streetwriters.notesnook.datatypes.Note; import java.util.ArrayList; @@ -138,7 +136,7 @@ public class RCTNNativeModule extends ReactContextBaseJavaModule { if (Objects.equals(extras.getString(IntentType), "NewReminder")) { map.putString(ReminderWidgetProvider.NewReminder, extras.getString(ReminderWidgetProvider.NewReminder)); } else if (Objects.equals(extras.getString(IntentType), "OpenReminder")) { - map.putString(ReminderViewsService.OpenReminderId, extras.getString(ReminderViewsService.OpenReminderId)); + map.putString(ReminderWidgetProvider.OpenReminderId, extras.getString(ReminderWidgetProvider.OpenReminderId)); } else if (Objects.equals(extras.getString(IntentType), "OpenNote")) { map.putString(NotePreviewWidget.OpenNoteId, extras.getString(NotePreviewWidget.OpenNoteId)); } @@ -156,14 +154,8 @@ public class RCTNNativeModule extends ReactContextBaseJavaModule { @ReactMethod public void getWidgetNotes(Promise promise) { - SharedPreferences pref = getReactApplicationContext().getSharedPreferences("appPreview", Context.MODE_PRIVATE); - Map map = pref.getAll(); WritableArray arr = Arguments.createArray(); - for(Map.Entry entry : map.entrySet()){ - if (entry.getKey().equals("remindersList")) continue; - String value = (String) entry.getValue(); - Gson gson = new Gson(); - Note note = gson.fromJson(value, Note.class); + for (Note note : WidgetUtils.getWidgetNotes(getReactApplicationContext()).values()) { arr.pushString(note.getId()); } promise.resolve(arr); @@ -171,36 +163,46 @@ public class RCTNNativeModule extends ReactContextBaseJavaModule { @ReactMethod public void hasWidgetNote(final String noteId, Promise promise) { - SharedPreferences pref = getReactApplicationContext().getSharedPreferences("appPreview", Context.MODE_PRIVATE); - Map map = pref.getAll(); boolean found = false; - for(Map.Entry entry : map.entrySet()){ - String value = (String) entry.getValue(); - if (value.contains(noteId)) { + for (Note note : WidgetUtils.getWidgetNotes(getReactApplicationContext()).values()) { + if (note.getId().equals(noteId)) { found = true; + break; } } promise.resolve(found); } + @ReactMethod public void updateWidgetNote(final String noteId, final String data) { - SharedPreferences pref = getReactApplicationContext().getSharedPreferences("appPreview", Context.MODE_PRIVATE); - Map map = pref.getAll(); + SharedPreferences pref = getReactApplicationContext().getSharedPreferences(WidgetUtils.PREFERENCES, Context.MODE_PRIVATE); SharedPreferences.Editor edit = pref.edit(); - ArrayList ids = new ArrayList<>(); - for(Map.Entry entry : map.entrySet()) { - String value = (String) entry.getValue(); - if (value.contains(noteId)) { - edit.putString(entry.getKey(), data); - ids.add(entry.getKey()); - } + List ids = new ArrayList<>(); + + // Match on the note's id, not on the raw JSON containing it somewhere: a note whose body + // happens to mention another note's id is not the same note. + for (Map.Entry entry : WidgetUtils.getWidgetNotes(getReactApplicationContext()).entrySet()) { + if (!noteId.equals(entry.getValue().getId())) continue; + edit.putString(String.valueOf(entry.getKey()), data); + ids.add(entry.getKey()); } edit.apply(); - for (String id: ids) { - NotePreviewWidget.updateAppWidget(mContext, AppWidgetManager.getInstance(mContext), Integer.parseInt(id)); + + for (int id : ids) { + NotePreviewWidget.updateAppWidget(mContext, AppWidgetManager.getInstance(mContext), id); } } + /** + * Redraws every widget from scratch. Needed because the app can be stopped while its widgets + * stay on the home screen: clearing app data empties the store without the widgets ever being + * told, so they keep showing content that is gone until something forces a redraw. + */ + @ReactMethod + public void refreshWidgets() { + WidgetUtils.refreshAll(mContext); + } + @ReactMethod public void updateReminderWidget() { AppWidgetManager wm = AppWidgetManager.getInstance(mContext); @@ -208,8 +210,8 @@ public class RCTNNativeModule extends ReactContextBaseJavaModule { for (int id: ids) { Log.d("Reminders", "Updating" + id); RemoteViews views = new RemoteViews(mContext.getPackageName(), R.layout.widget_reminders); + // The rows are part of this update, so there is nothing left to invalidate afterwards. ReminderWidgetProvider.updateAppWidget(mContext, wm, id, views); - wm.notifyAppWidgetViewDataChanged(id, R.id.widget_list_view); } } diff --git a/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/ReminderViewsService.java b/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/ReminderViewsService.java deleted file mode 100644 index 5e9416e39..000000000 --- a/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/ReminderViewsService.java +++ /dev/null @@ -1,106 +0,0 @@ -package com.streetwriters.notesnook; - -import android.app.ActivityOptions; -import android.app.PendingIntent; -import android.content.Intent; -import android.content.SharedPreferences; -import android.net.Uri; -import android.os.Build; -import android.os.Bundle; -import android.util.Log; -import android.widget.RemoteViewsService; -import android.content.Context; -import android.widget.RemoteViews; -import com.google.gson.Gson; -import com.google.gson.reflect.TypeToken; -import com.streetwriters.notesnook.datatypes.Reminder; - -import java.util.ArrayList; -import java.util.List; - -public class ReminderViewsService extends RemoteViewsService { - static String OpenReminderId = "com.streetwriters.notesnook.OpenReminderId"; - @Override - public RemoteViewsFactory onGetViewFactory(Intent intent) { - return new ReminderRemoteViewsFactory(this.getApplicationContext(), intent); - } -} - -class ReminderRemoteViewsFactory implements RemoteViewsService.RemoteViewsFactory { - private Context context; - private List reminders; - - public ReminderRemoteViewsFactory(Context context, Intent intent) { - this.context = context; - } - - @Override - public void onCreate() { - // Initialize reminders list - reminders = new ArrayList(); - } - - @Override - public void onDataSetChanged() { - reminders.clear(); - SharedPreferences preferences = context.getSharedPreferences("appPreview", Context.MODE_PRIVATE); - Gson gson = new Gson(); - reminders = gson.fromJson(preferences.getString("remindersList","[]"), new TypeToken>(){}.getType()); - - } - - @Override - public void onDestroy() { - reminders.clear(); - } - - @Override - public int getCount() { - return reminders.size(); - } - - @Override - public RemoteViews getViewAt(int position) { - Reminder reminder = reminders.get(position); - - boolean useMiniLayout = reminder.getDescription() == null || reminder.getDescription().isEmpty(); - - RemoteViews views = new RemoteViews(context.getPackageName(), useMiniLayout ? R.layout.widget_reminder_layout_small : R.layout.widget_reminder_layout); - - views.setTextViewText(R.id.reminder_title, reminder.getTitle()); - if (!useMiniLayout) { - views.setTextViewText(R.id.reminder_description, reminder.getDescription()); - } - views.setTextViewText(R.id.reminder_time, reminder.getFormattedTime()); - final Intent fillInIntent = new Intent(); - final Bundle extras = new Bundle(); - extras.putString(ReminderViewsService.OpenReminderId, reminder.getId()); - fillInIntent.setData(Uri.parse("https://app.notesnook.com/open_reminder?id=" + reminder.getId())); - fillInIntent.putExtra(RCTNNativeModule.IntentType, "OpenReminder"); - fillInIntent.putExtras(extras); - views.setOnClickFillInIntent(R.id.reminder_item_btn, fillInIntent); - return views; - } - - - @Override - public RemoteViews getLoadingView() { - return null; - } - - @Override - public int getViewTypeCount() { - return 2; - } - - @Override - public long getItemId(int position) { - - return position; - } - - @Override - public boolean hasStableIds() { - return true; - } -} \ No newline at end of file diff --git a/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/ReminderWidgetProvider.java b/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/ReminderWidgetProvider.java index 3ecd4daed..bcbfb0233 100644 --- a/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/ReminderWidgetProvider.java +++ b/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/ReminderWidgetProvider.java @@ -1,18 +1,22 @@ package com.streetwriters.notesnook; -import android.app.ActivityOptions; import android.app.PendingIntent; import android.appwidget.AppWidgetManager; import android.appwidget.AppWidgetProvider; import android.content.Context; import android.content.Intent; import android.net.Uri; -import android.os.Build; -import android.os.Bundle; import android.widget.RemoteViews; +import androidx.core.widget.RemoteViewsCompat; + +import com.streetwriters.notesnook.datatypes.Reminder; + +import java.util.List; + public class ReminderWidgetProvider extends AppWidgetProvider { static String NewReminder = "com.streetwriters.notesnook.NewReminder"; + static String OpenReminderId = "com.streetwriters.notesnook.OpenReminderId"; @Override public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { @@ -23,20 +27,10 @@ public class ReminderWidgetProvider extends AppWidgetProvider { } - private static Bundle getActivityOptionsBundle() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { - ActivityOptions activityOptions = ActivityOptions.makeBasic(); - activityOptions.setPendingIntentCreatorBackgroundActivityStartMode( - ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED); - return activityOptions.toBundle(); - } else - return null; - } - static void updateAppWidget(Context context, AppWidgetManager appWidgetManager, int appWidgetId, RemoteViews views) { Intent listview_intent_template = new Intent(context, MainActivity.class); listview_intent_template.setAction(Intent.ACTION_VIEW); - PendingIntent pendingIntent = PendingIntent.getActivity(context, appWidgetId, listview_intent_template, PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_MUTABLE, getActivityOptionsBundle()); + PendingIntent pendingIntent = PendingIntent.getActivity(context, appWidgetId, listview_intent_template, PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_MUTABLE, WidgetUtils.getActivityOptionsBundle()); views.setPendingIntentTemplate(R.id.widget_list_view, pendingIntent); Intent new_reminder_intent = new Intent(context, MainActivity.class); @@ -44,13 +38,31 @@ public class ReminderWidgetProvider extends AppWidgetProvider { new_reminder_intent.setAction(Intent.ACTION_VIEW); new_reminder_intent.putExtra(RCTNNativeModule.IntentType, "NewReminder"); new_reminder_intent.setData(Uri.parse("https://app.notesnook.com/new_reminder")); - PendingIntent pendingIntent2 = PendingIntent.getActivity(context, appWidgetId, new_reminder_intent, PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE, getActivityOptionsBundle()); + PendingIntent pendingIntent2 = PendingIntent.getActivity(context, appWidgetId, new_reminder_intent, PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE, WidgetUtils.getActivityOptionsBundle()); views.setOnClickPendingIntent(R.id.add_button, pendingIntent2); - Intent list_remote_adapter_intent = new Intent(context, ReminderViewsService.class); - list_remote_adapter_intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId); - views.setRemoteAdapter(R.id.widget_list_view, list_remote_adapter_intent); + // The rows travel with the update itself, so there is no bound service to keep in sync and + // nothing to invalidate separately: every update redraws from the current data. + List reminders = WidgetUtils.getWidgetReminders(context); + RemoteViewsCompat.RemoteCollectionItems.Builder items = + new RemoteViewsCompat.RemoteCollectionItems.Builder(); + for (Reminder reminder : reminders) { + items.addItem(getItemId(reminder), WidgetUtils.createReminderItem(context, reminder)); + } + // Two, because a reminder without a description uses the compact row layout. + items.setViewTypeCount(2); + items.setHasStableIds(true); + + RemoteViewsCompat.setRemoteAdapter(context, views, appWidgetId, R.id.widget_list_view, items.build()); views.setEmptyView(R.id.widget_list_view, R.id.empty_view); appWidgetManager.updateAppWidget(appWidgetId, views); } -} \ No newline at end of file + + /** + * Ties a row to its reminder rather than to its position, so rows keep their identity when the + * list shifts around them. + */ + private static long getItemId(Reminder reminder) { + return reminder.getId() == null ? 0 : reminder.getId().hashCode(); + } +} diff --git a/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/WidgetTimeChangeReceiver.java b/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/WidgetTimeChangeReceiver.java new file mode 100644 index 000000000..957f6769a --- /dev/null +++ b/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/WidgetTimeChangeReceiver.java @@ -0,0 +1,28 @@ +package com.streetwriters.notesnook; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; + +/** + * Redraws the widgets when the clock or timezone changes. + * + * Reminder rows are described relative to the current time ("Upcoming"/"Last", "Today"/"Tomorrow"), + * and a reminder that has passed drops off the list entirely. All of that is decided when the + * widget is drawn, so moving the clock leaves the previous drawing in place: a reminder can still + * read "Upcoming" long after its time has gone by. + * + * The app normally redraws the list when a reminder notification is delivered, but that does not + * happen if the reminder never fires, which is exactly the case when the clock jumps past it. + * + * Only TIME_SET and TIMEZONE_CHANGED are handled here: DATE_CHANGED is not exempt from the + * Android 8 limits on manifest-registered implicit broadcasts, so a receiver for it would never + * run. Crossing midnight is instead picked up by the widget's own periodic update. + */ +public class WidgetTimeChangeReceiver extends BroadcastReceiver { + + @Override + public void onReceive(Context context, Intent intent) { + WidgetUtils.refreshAll(context); + } +} diff --git a/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/WidgetUtils.java b/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/WidgetUtils.java new file mode 100644 index 000000000..c23d55c44 --- /dev/null +++ b/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/WidgetUtils.java @@ -0,0 +1,299 @@ +package com.streetwriters.notesnook; + +import android.app.ActivityOptions; +import android.appwidget.AppWidgetManager; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.text.format.DateUtils; +import android.util.Log; +import android.widget.RemoteViews; + +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; +import com.streetwriters.notesnook.datatypes.Note; +import com.streetwriters.notesnook.datatypes.Reminder; + +import java.util.ArrayList; +import java.util.Calendar; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.TimeUnit; + +/** + * Shared helpers for the home screen widgets. + */ +public class WidgetUtils { + + static final String PREFERENCES = "appPreview"; + static final String REMINDERS_KEY = "remindersList"; + + /** + * Every row is serialized into the widget update itself, which has to fit inside a binder + * transaction, so the list cannot grow without bound. Far more than fits on screen anyway. + */ + private static final int MAX_REMINDERS = 50; + + /** + * Redraws every widget that currently exists, and drops stored notes for widgets that no + * longer do. + * + * Everything else keys off what we have stored, which is fine while the app is running but + * leaves widgets showing content that no longer exists once the store is emptied underneath + * them (clearing app data) or a widget is removed while the app is stopped (onDeleted never + * arrives). Starting from the widgets the system knows about, rather than from our own data, + * is what makes this self-correcting. + * + * NoteWidget is left alone deliberately: it is a static button with no stored state, and its + * layout depends on the size it was last given. + */ + static void refreshAll(Context context) { + AppWidgetManager manager = AppWidgetManager.getInstance(context); + + int[] noteWidgetIds = manager.getAppWidgetIds( + new ComponentName(context, NotePreviewWidget.class)); + removeOrphanedNotes(context, noteWidgetIds); + for (int appWidgetId : noteWidgetIds) { + NotePreviewWidget.updateAppWidget(context, manager, appWidgetId); + } + + for (int appWidgetId : manager.getAppWidgetIds( + new ComponentName(context, ReminderWidgetProvider.class))) { + RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.widget_reminders); + ReminderWidgetProvider.updateAppWidget(context, manager, appWidgetId, views); + } + } + + /** + * Drops stored notes whose widget is gone, so the preferences file cannot grow forever. + */ + private static void removeOrphanedNotes(Context context, int[] liveWidgetIds) { + Set live = new HashSet<>(); + for (int appWidgetId : liveWidgetIds) live.add(String.valueOf(appWidgetId)); + + SharedPreferences preferences = context.getSharedPreferences(PREFERENCES, Context.MODE_PRIVATE); + SharedPreferences.Editor edit = preferences.edit(); + boolean changed = false; + + for (String key : preferences.getAll().keySet()) { + // Leave anything that is not a widget id alone, the reminders list included. + if (parseWidgetId(key) == null || live.contains(key)) continue; + edit.remove(key); + changed = true; + } + if (changed) edit.apply(); + } + + /** + * The note each note widget is showing, keyed by widget id. + * + * The preferences file mixes two things: one note per widget id, and the reminders list under + * its own key. Only numeric keys are widget notes, so anything else is skipped rather than + * being treated as a note. + */ + static Map getWidgetNotes(Context context) { + SharedPreferences preferences = context.getSharedPreferences(PREFERENCES, Context.MODE_PRIVATE); + Map notes = new LinkedHashMap<>(); + + for (Map.Entry entry : preferences.getAll().entrySet()) { + Integer widgetId = parseWidgetId(entry.getKey()); + if (widgetId == null) continue; + if (!(entry.getValue() instanceof String)) continue; + + Note note = parseNote((String) entry.getValue()); + if (note == null || note.getId() == null) continue; + notes.put(widgetId, note); + } + return notes; + } + + /** + * The widget id a preferences key refers to, or null if the key is not a widget id at all. + */ + private static Integer parseWidgetId(String key) { + try { + return Integer.valueOf(key); + } catch (NumberFormatException e) { + return null; + } + } + + static Note parseNote(String data) { + if (data == null || data.isEmpty()) return null; + try { + return new Gson().fromJson(data, Note.class); + } catch (Exception e) { + Log.e("NotePreviewWidget", "Could not read a stored note", e); + return null; + } + } + + /** + * The reminders the app last wrote out, minus any that have now dropped out of view. Reading + * and filtering happens here so the provider can push the rows straight into the widget. + */ + static List getWidgetReminders(Context context) { + SharedPreferences preferences = context.getSharedPreferences(PREFERENCES, Context.MODE_PRIVATE); + List stored = null; + try { + stored = new Gson().fromJson(preferences.getString(REMINDERS_KEY, "[]"), + new TypeToken>() {}.getType()); + } catch (Exception e) { + Log.e("Reminders", "Could not read the stored reminders list", e); + } + + List active = new ArrayList<>(); + if (stored == null) return active; + + for (Reminder reminder : stored) { + if (!isVisibleInWidget(reminder)) continue; + if (active.size() >= MAX_REMINDERS) { + Log.w("Reminders", "Widget list truncated to " + MAX_REMINDERS + " reminders"); + break; + } + active.add(reminder); + } + return active; + } + + /** + * Builds a single row of the reminders list. + */ + static RemoteViews createReminderItem(Context context, Reminder reminder) { + boolean useMiniLayout = reminder.getDescription() == null || reminder.getDescription().isEmpty(); + + RemoteViews views = new RemoteViews(context.getPackageName(), + useMiniLayout ? R.layout.widget_reminder_layout_small : R.layout.widget_reminder_layout); + + views.setTextViewText(R.id.reminder_title, reminder.getTitle()); + if (!useMiniLayout) { + views.setTextViewText(R.id.reminder_description, reminder.getDescription()); + } + views.setTextViewText(R.id.reminder_time, formatReminderTime(context, reminder)); + + Intent fillInIntent = new Intent(); + fillInIntent.setData(Uri.parse("https://app.notesnook.com/open_reminder?id=" + reminder.getId())); + fillInIntent.putExtra(RCTNNativeModule.IntentType, "OpenReminder"); + fillInIntent.putExtra(ReminderWidgetProvider.OpenReminderId, reminder.getId()); + views.setOnClickFillInIntent(R.id.reminder_item_btn, fillInIntent); + return views; + } + + /** + * Options attached to the PendingIntents our widgets hand to the launcher, opting the creator + * (us) in to background activity starts so a tap on the widget can bring up an activity. + * + * MODE_BACKGROUND_ACTIVITY_START_ALLOWED is deprecated since API 36 and Android 17 extends the + * background activity launch restrictions to IntentSender, so on API 36+ we use the narrower + * MODE_BACKGROUND_ACTIVITY_START_ALLOW_IF_VISIBLE instead. That is enough for widgets: the + * sender is the launcher, which is visible whenever the user taps the widget. + */ + static Bundle getActivityOptionsBundle() { + ActivityOptions activityOptions; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.BAKLAVA) { + activityOptions = ActivityOptions.makeBasic(); + activityOptions.setPendingIntentCreatorBackgroundActivityStartMode( + ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_IF_VISIBLE); + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + activityOptions = ActivityOptions.makeBasic(); + activityOptions.setPendingIntentCreatorBackgroundActivityStartMode( + ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED); + } else { + return null; + } + return activityOptions.toBundle(); + } + + /** + * How long a reminder keeps its place in the list after going off, so the user can see that it + * happened rather than watching it vanish. Must match RECENTLY_PASSED_WINDOW in + * services/notifications.ts, which decides what gets written out in the first place. + */ + private static final long RECENTLY_PASSED_WINDOW_MS = TimeUnit.HOURS.toMillis(3); + + /** + * Whether a reminder should still be drawn. + * + * We re-check here rather than trusting the stored list because that list is only rewritten + * while the app runs. This is what actually retires a reminder once its grace period is up: + * every redraw re-evaluates it against the current time. + */ + static boolean isVisibleInWidget(Reminder reminder) { + if (reminder == null) return false; + if (reminder.isDisabled()) return false; + + long now = System.currentTimeMillis(); + if (reminder.getSnoozeUntil() > now) return true; + if (!"once".equals(reminder.getMode())) return true; + + long triggerDate = reminder.getTriggerDate() > 0 ? reminder.getTriggerDate() : reminder.getDate(); + return triggerDate > now - RECENTLY_PASSED_WINDOW_MS; + } + + /** + * Builds the label shown under a reminder. The app sends us the absolute trigger time plus the + * parts that never change ("5:00 PM", "12-05-2026, 5:00 PM"); everything that depends on the + * current time is decided here so it stays right as the widget redraws. + * + * Falls back to the pre-formatted string for lists written by an older version of the app. + */ + static String formatReminderTime(Context context, Reminder reminder) { + long triggerDate = reminder.getTriggerDate(); + String timeOfDay = reminder.getFormattedTimeOfDay(); + if (triggerDate <= 0 || timeOfDay == null || timeOfDay.isEmpty()) { + return reminder.getFormattedTime(); + } + + if ("permanent".equals(reminder.getMode())) { + return context.getString(R.string.reminder_ongoing); + } + + long now = System.currentTimeMillis(); + if (reminder.getSnoozeUntil() > now) { + return context.getString(R.string.reminder_snoozed_until, timeOfDay); + } + + String text; + long dayOffset = daysFromToday(triggerDate, now); + if (dayOffset == 0) { + text = context.getString(R.string.reminder_today, timeOfDay); + } else if (dayOffset == 1) { + text = context.getString(R.string.reminder_tomorrow, timeOfDay); + } else if (dayOffset == -1) { + text = context.getString(R.string.reminder_yesterday, timeOfDay); + } else { + text = reminder.getFormattedDateTime(); + if (text == null || text.isEmpty()) return reminder.getFormattedTime(); + } + + return context.getString( + triggerDate <= now ? R.string.reminder_last : R.string.reminder_upcoming, text); + } + + /** + * Calendar days between two instants. Compares midnights rather than subtracting the raw + * difference so that "tomorrow" is still tomorrow across a DST change or just before midnight. + */ + private static long daysFromToday(long time, long now) { + long target = startOfDay(time); + long today = startOfDay(now); + return Math.round((target - today) / (double) DateUtils.DAY_IN_MILLIS); + } + + private static long startOfDay(long time) { + Calendar calendar = Calendar.getInstance(); + calendar.setTimeInMillis(time); + calendar.set(Calendar.HOUR_OF_DAY, 0); + calendar.set(Calendar.MINUTE, 0); + calendar.set(Calendar.SECOND, 0); + calendar.set(Calendar.MILLISECOND, 0); + return calendar.getTimeInMillis(); + } +} diff --git a/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/datatypes/Reminder.java b/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/datatypes/Reminder.java index 715199ca6..9b9b0fac6 100644 --- a/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/datatypes/Reminder.java +++ b/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/datatypes/Reminder.java @@ -2,13 +2,14 @@ package com.streetwriters.notesnook.datatypes; import androidx.annotation.Keep; -import java.util.concurrent.TimeUnit; - @Keep public class Reminder extends BaseItem { private String title; private String description; private String formattedTime; + private String formattedTimeOfDay; // e.g. "5:00 PM" + private String formattedDateTime; // e.g. "12-05-2026, 5:00 PM" + private long triggerDate; // absolute time this reminder next fires private String priority; // "silent", "vibrate", "urgent" private long date; private String mode; // "repeat", "once", "permanent" @@ -107,32 +108,27 @@ public class Reminder extends BaseItem { this.formattedTime = formattedTime; } - public String formatTime(long timeInMillis) { - long currentTime = System.currentTimeMillis(); - long diff = timeInMillis - currentTime; + public String getFormattedTimeOfDay() { + return formattedTimeOfDay; + } - if (diff < TimeUnit.MINUTES.toMillis(1)) { - return "in " + (diff / 1000) + " seconds"; - } else if (diff < TimeUnit.HOURS.toMillis(1)) { - long minutes = TimeUnit.MILLISECONDS.toMinutes(diff); - return "in " + minutes + " minute" + (minutes > 1 ? "s" : ""); - } else if (diff < TimeUnit.DAYS.toMillis(1)) { - long hours = TimeUnit.MILLISECONDS.toHours(diff); - return "in " + hours + " hour" + (hours > 1 ? "s" : ""); - } else if (diff < TimeUnit.DAYS.toMillis(2)) { - return "tomorrow"; - } else if (diff < TimeUnit.DAYS.toMillis(7)) { - long days = TimeUnit.MILLISECONDS.toDays(diff); - return "in " + days + " day" + (days > 1 ? "s" : ""); - } else if (diff < TimeUnit.DAYS.toMillis(30)) { - long weeks = TimeUnit.MILLISECONDS.toDays(diff) / 7; - return "in " + weeks + " week" + (weeks > 1 ? "s" : ""); - } else if (diff < TimeUnit.DAYS.toMillis(365)) { - long months = TimeUnit.MILLISECONDS.toDays(diff) / 30; - return "in " + months + " month" + (months > 1 ? "s" : ""); - } else { - long years = TimeUnit.MILLISECONDS.toDays(diff) / 365; - return "in " + years + " year" + (years > 1 ? "s" : ""); - } + public void setFormattedTimeOfDay(String formattedTimeOfDay) { + this.formattedTimeOfDay = formattedTimeOfDay; + } + + public String getFormattedDateTime() { + return formattedDateTime; + } + + public void setFormattedDateTime(String formattedDateTime) { + this.formattedDateTime = formattedDateTime; + } + + public long getTriggerDate() { + return triggerDate; + } + + public void setTriggerDate(long triggerDate) { + this.triggerDate = triggerDate; } } \ No newline at end of file diff --git a/apps/mobile/android/app/src/main/res/drawable/layout_bg.xml b/apps/mobile/android/app/src/main/res/drawable/layout_bg.xml index 4e9108ff5..af55a9847 100644 --- a/apps/mobile/android/app/src/main/res/drawable/layout_bg.xml +++ b/apps/mobile/android/app/src/main/res/drawable/layout_bg.xml @@ -2,6 +2,6 @@ - + \ No newline at end of file diff --git a/apps/mobile/android/app/src/main/res/layout/new_note_widget.xml b/apps/mobile/android/app/src/main/res/layout/new_note_widget.xml index 622886bd1..70bb73b1b 100644 --- a/apps/mobile/android/app/src/main/res/layout/new_note_widget.xml +++ b/apps/mobile/android/app/src/main/res/layout/new_note_widget.xml @@ -2,8 +2,7 @@ xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent" - android:background="@android:color/transparent" - android:theme="@style/ThemeOverlay.Notesnook.AppWidgetContainer"> + android:background="@android:color/transparent"> + android:background="@android:color/transparent"> + android:text="@string/widget_note_unconfigured_title" /> + android:text="@string/widget_note_unconfigured_body" /> \ No newline at end of file diff --git a/apps/mobile/android/app/src/main/res/layout/note_widget_preview.xml b/apps/mobile/android/app/src/main/res/layout/note_widget_preview.xml new file mode 100644 index 000000000..35c3cd7c6 --- /dev/null +++ b/apps/mobile/android/app/src/main/res/layout/note_widget_preview.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + diff --git a/apps/mobile/android/app/src/main/res/layout/widget_reminder_empty.xml b/apps/mobile/android/app/src/main/res/layout/widget_reminder_empty.xml deleted file mode 100644 index 9be87bb7a..000000000 --- a/apps/mobile/android/app/src/main/res/layout/widget_reminder_empty.xml +++ /dev/null @@ -1,8 +0,0 @@ - \ No newline at end of file diff --git a/apps/mobile/android/app/src/main/res/layout/widget_reminders.xml b/apps/mobile/android/app/src/main/res/layout/widget_reminders.xml index f7f25bf55..10b723840 100644 --- a/apps/mobile/android/app/src/main/res/layout/widget_reminders.xml +++ b/apps/mobile/android/app/src/main/res/layout/widget_reminders.xml @@ -12,22 +12,23 @@ android:orientation="horizontal" android:paddingHorizontal="12dp" android:layout_gravity="center" - android:paddingTop="12dp" + android:paddingTop="8dp" + android:paddingBottom="8dp" > + android:text="Reminders"/> @@ -57,7 +58,8 @@ android:layout_height="match_parent" android:textAlignment="center" android:gravity="center" - android:text="Tap on + to add reminder"/> + android:textColor="@color/text" + android:text="@string/widget_reminders_empty"/> diff --git a/apps/mobile/android/app/src/main/res/layout/widget_reminders_preview.xml b/apps/mobile/android/app/src/main/res/layout/widget_reminders_preview.xml new file mode 100644 index 000000000..92fd23a21 --- /dev/null +++ b/apps/mobile/android/app/src/main/res/layout/widget_reminders_preview.xml @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/mobile/android/app/src/main/res/values-night/colors.xml b/apps/mobile/android/app/src/main/res/values-night/colors.xml index f1c79618b..3b836d0a6 100644 --- a/apps/mobile/android/app/src/main/res/values-night/colors.xml +++ b/apps/mobile/android/app/src/main/res/values-night/colors.xml @@ -1,9 +1,5 @@ - #FFE1F5FE - #FF81D4FA - #FF039BE5 - #FF01579B #1f1f1f #1D1D1D #2E2E2E diff --git a/apps/mobile/android/app/src/main/res/values-v31/dimens.xml b/apps/mobile/android/app/src/main/res/values-v31/dimens.xml new file mode 100644 index 000000000..c3e87c9e4 --- /dev/null +++ b/apps/mobile/android/app/src/main/res/values-v31/dimens.xml @@ -0,0 +1,8 @@ + + + + + @android:dimen/system_app_widget_background_radius + + diff --git a/apps/mobile/android/app/src/main/res/values/attrs.xml b/apps/mobile/android/app/src/main/res/values/attrs.xml deleted file mode 100644 index 97531a256..000000000 --- a/apps/mobile/android/app/src/main/res/values/attrs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/apps/mobile/android/app/src/main/res/values/colors.xml b/apps/mobile/android/app/src/main/res/values/colors.xml index 4ac8bc762..ab4d9ecc4 100644 --- a/apps/mobile/android/app/src/main/res/values/colors.xml +++ b/apps/mobile/android/app/src/main/res/values/colors.xml @@ -1,9 +1,5 @@ - #FFE1F5FE - #FF81D4FA - #FF039BE5 - #FF01579B #FFFFFF #DCEDEDED #BFBFBF diff --git a/apps/mobile/android/app/src/main/res/values/dimens.xml b/apps/mobile/android/app/src/main/res/values/dimens.xml index 4db8c5906..7aa2b16bc 100644 --- a/apps/mobile/android/app/src/main/res/values/dimens.xml +++ b/apps/mobile/android/app/src/main/res/values/dimens.xml @@ -7,4 +7,8 @@ http://developer.android.com/guide/topics/appwidgets/index.html#CreatingLayout --> 0dp + + 10dp + \ No newline at end of file diff --git a/apps/mobile/android/app/src/main/res/values/strings.xml b/apps/mobile/android/app/src/main/res/values/strings.xml index 1a233ee09..d5b719bcd 100644 --- a/apps/mobile/android/app/src/main/res/values/strings.xml +++ b/apps/mobile/android/app/src/main/res/values/strings.xml @@ -4,9 +4,28 @@ EXAMPLE Add widget Take a quick note. - Quick overview of upcoming reminders + Quick overview of reminders Reminders Note Add a note to home screen Quick note + Tap + to add a reminder + + + Meeting notes + Discuss the roadmap and agree on timelines. + Take a walk + Upcoming: Today, 5:00 PM + Call the dentist + Upcoming: Tomorrow, 9:00 AM + + Tap to choose a note + Pick the note you want shown here. + Ongoing + Snoozed until %1$s + Today, %1$s + Tomorrow, %1$s + Yesterday, %1$s + Upcoming: %1$s + Last: %1$s diff --git a/apps/mobile/android/app/src/main/res/values/themes.xml b/apps/mobile/android/app/src/main/res/values/themes.xml deleted file mode 100644 index 935088e01..000000000 --- a/apps/mobile/android/app/src/main/res/values/themes.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - \ No newline at end of file diff --git a/apps/mobile/android/app/src/main/res/xml-v36/note_widget_info.xml b/apps/mobile/android/app/src/main/res/xml-v36/note_widget_info.xml new file mode 100644 index 000000000..97a47ff1a --- /dev/null +++ b/apps/mobile/android/app/src/main/res/xml-v36/note_widget_info.xml @@ -0,0 +1,22 @@ + + \ No newline at end of file diff --git a/apps/mobile/android/app/src/main/res/xml-v36/widget_reminders_info.xml b/apps/mobile/android/app/src/main/res/xml-v36/widget_reminders_info.xml new file mode 100644 index 000000000..653f52e8f --- /dev/null +++ b/apps/mobile/android/app/src/main/res/xml-v36/widget_reminders_info.xml @@ -0,0 +1,15 @@ + \ No newline at end of file diff --git a/apps/mobile/android/app/src/main/res/xml/new_note_widget_info.xml b/apps/mobile/android/app/src/main/res/xml/new_note_widget_info.xml index 086bf7228..c76d158b6 100644 --- a/apps/mobile/android/app/src/main/res/xml/new_note_widget_info.xml +++ b/apps/mobile/android/app/src/main/res/xml/new_note_widget_info.xml @@ -10,6 +10,7 @@ android:targetCellWidth="5" android:targetCellHeight="1" android:previewImage="@drawable/widget_preview" + android:previewLayout="@layout/new_note_widget" android:resizeMode="horizontal|vertical" android:updatePeriodMillis="86400000" android:widgetCategory="home_screen"/> \ No newline at end of file diff --git a/apps/mobile/android/app/src/main/res/xml/note_widget_info.xml b/apps/mobile/android/app/src/main/res/xml/note_widget_info.xml index 84c936b29..c597dc450 100644 --- a/apps/mobile/android/app/src/main/res/xml/note_widget_info.xml +++ b/apps/mobile/android/app/src/main/res/xml/note_widget_info.xml @@ -12,6 +12,7 @@ android:targetCellWidth="5" android:targetCellHeight="1" android:previewImage="@drawable/note_widget_preview" + android:previewLayout="@layout/note_widget_preview" android:resizeMode="horizontal|vertical" android:updatePeriodMillis="86400000" android:widgetCategory="home_screen"/> \ No newline at end of file diff --git a/apps/mobile/android/app/src/main/res/xml/widget_reminders_info.xml b/apps/mobile/android/app/src/main/res/xml/widget_reminders_info.xml index e9c87362f..175f46c39 100644 --- a/apps/mobile/android/app/src/main/res/xml/widget_reminders_info.xml +++ b/apps/mobile/android/app/src/main/res/xml/widget_reminders_info.xml @@ -1,14 +1,15 @@ \ No newline at end of file diff --git a/apps/mobile/app/hooks/use-app-events.tsx b/apps/mobile/app/hooks/use-app-events.tsx index 91dbdde97..8b15aa470 100644 --- a/apps/mobile/app/hooks/use-app-events.tsx +++ b/apps/mobile/app/hooks/use-app-events.tsx @@ -81,6 +81,7 @@ import { setUpdateAvailableMessage } from "../services/message"; import Navigation from "../services/navigation"; +import { NotePreviewWidget } from "../services/note-preview-widget"; import Notifications from "../services/notifications"; import PremiumService from "../services/premium"; import SettingsService from "../services/settings"; @@ -575,6 +576,11 @@ export const useAppEvents = () => { useEffect(() => { if (isAppLoading) return; + // Widgets outlive the app process, so they can be left showing content the app no longer has + // (most obviously after the user clears app data). Nothing can run at that moment, so the + // first launch afterwards is the earliest chance to put them right. + NotePreviewWidget.updateNotes(); + let subscriptions: EventManagerSubscription[] = []; const eventManager = db.eventManager; subscriptions = [ diff --git a/apps/mobile/app/services/note-preview-widget.ts b/apps/mobile/app/services/note-preview-widget.ts index 8f7610732..cbb259e6a 100644 --- a/apps/mobile/app/services/note-preview-widget.ts +++ b/apps/mobile/app/services/note-preview-widget.ts @@ -34,6 +34,10 @@ export const NotePreviewWidget = { NotesnookModule.updateWidgetNote(id, JSON.stringify(newNote)); } + // Redraw from the widgets that actually exist rather than only the ones we + // have notes for. After app data is cleared there are none, and the widgets + // would otherwise keep showing content that no longer exists. + NotesnookModule.refreshWidgets(); }, 500); }, updateNote: async (id: string, note: Note) => { diff --git a/apps/mobile/app/services/notifications.ts b/apps/mobile/app/services/notifications.ts index 73ffa8e45..6dd620b5c 100644 --- a/apps/mobile/app/services/notifications.ts +++ b/apps/mobile/app/services/notifications.ts @@ -17,8 +17,12 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . */ -import { getFormattedReminderTime } from "@notesnook/common"; -import { isReminderActive, Reminder } from "@notesnook/core"; +import { getFormattedDate, getFormattedReminderTime } from "@notesnook/common"; +import { + getUpcomingReminderTime, + isReminderActive, + Reminder +} from "@notesnook/core"; import { strings } from "@notesnook/intl"; import notifee, { AndroidStyle, @@ -262,8 +266,13 @@ const onEvent = async ({ type, detail }: Event) => { type ReminderWithFormattedTime = Reminder & { formattedTime?: string; + triggerDate?: number; + formattedTimeOfDay?: string; + formattedDateTime?: string; }; +const RECENTLY_PASSED_WINDOW = 3 * 60 * 60 * 1000; + async function updateRemindersForWidget() { if (Platform.OS === "ios") return; const reminders: ReminderWithFormattedTime[] = await db.reminders?.all.items( @@ -273,18 +282,33 @@ async function updateRemindersForWidget() { sortDirection: "asc" } ); - const activeReminders = []; + const widgetReminders = []; if (!reminders) return; for (const reminder of reminders) { - if (isReminderActive(reminder)) { - reminder.formattedTime = getFormattedReminderTime(reminder); - activeReminders.push(reminder); - } + const triggerDate = + reminder.snoozeUntil && reminder.snoozeUntil > Date.now() + ? reminder.snoozeUntil + : reminder.mode === "repeat" + ? getUpcomingReminderTime(reminder) + : reminder.date; + + const recentlyPassed = + reminder.mode === "once" && + !reminder.disabled && + triggerDate > Date.now() - RECENTLY_PASSED_WINDOW; + + if (!isReminderActive(reminder) && !recentlyPassed) continue; + + reminder.triggerDate = triggerDate; + reminder.formattedTimeOfDay = getFormattedDate(triggerDate, "time"); + reminder.formattedDateTime = getFormattedDate(triggerDate, "date-time"); + reminder.formattedTime = getFormattedReminderTime(reminder); + widgetReminders.push(reminder); } NotesnookModule.setString( "appPreview", "remindersList", - JSON.stringify(activeReminders) + JSON.stringify(widgetReminders) ); NotesnookModule.updateReminderWidget(); } diff --git a/apps/mobile/app/utils/notesnook-module.ts b/apps/mobile/app/utils/notesnook-module.ts index d682e3f28..0d90c4681 100644 --- a/apps/mobile/app/utils/notesnook-module.ts +++ b/apps/mobile/app/utils/notesnook-module.ts @@ -45,6 +45,7 @@ interface NotesnookModuleInterface { hasWidgetNote: (noteId: string) => Promise; updateWidgetNote: (noteId: string, data: string) => void; updateReminderWidget: () => void; + refreshWidgets: () => void; isGestureNavigationEnabled: () => boolean; addShortcut: ( id: string, @@ -81,6 +82,7 @@ export const NotesnookModule: NotesnookModuleInterface = Platform.select({ hasWidgetNote: () => {}, updateWidgetNote: () => {}, updateReminderWidget: () => {}, + refreshWidgets: () => {}, isGestureNavigationEnabled: () => true, addShortcut: () => Promise.resolve(false), removeShortcut: () => Promise.resolve(false), diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 68056d730..2d27f43f5 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -30,6 +30,7 @@ export { type DatabaseUpdatedEvent } from "./database/index.js"; export { FilteredSelector } from "./database/sql-collection.js"; export { getUpcomingReminder, + getUpcomingReminderTime, formatReminderTime, isReminderToday, isReminderActive