📄 src/Activities/NoteListActivity.cs
using Android.Content;
using Android.Views;
using AndroidX.RecyclerView.Widget;
using NotesApp.Adapters;
using NotesApp.Graph;
using NotesApp.Models;
using NotesApp.Storage;

namespace NotesApp.Activities;

[Activity(Label = "@string/app_name", Exported = false)]
public class NoteListActivity : Activity
{
    private RecyclerView _recyclerView = null!;
    private ProgressBar _progressBar = null!;
    private NoteListAdapter _adapter = null!;
    private GraphService _graphService = null!;
    private bool _hasScrolledToBottom;

    protected override void OnCreate(Bundle? savedInstanceState)
    {
        base.OnCreate(savedInstanceState);
        SetContentView(Resource.Layout.activity_note_list);

        _recyclerView = FindViewById<RecyclerView>(Resource.Id.note_recycler_view)!;
        _recyclerView.SetLayoutManager(new LinearLayoutManager(this));

        _adapter = new NoteListAdapter(OnNoteTapped);
        _recyclerView.SetAdapter(_adapter);

        _progressBar = FindViewById<ProgressBar>(Resource.Id.note_refresh_progress)!;

        _graphService = new GraphService(this);

        // Cache renders immediately, scrolled to the bottom, before any network call.
        BindItems(NoteCacheStore.Load());

        _ = RefreshAsync();
    }

    private async Task RefreshAsync()
    {
        var folderId = AppSettings.RootFolderId;
        if (folderId is null)
            return;

        _progressBar.Visibility = ViewStates.Visible;
        try
        {
            var cacheById = NoteCacheStore.Load().ToDictionary(e => e.Id);

            var remoteNotes = await _graphService.ListMarkdownFilesAsync(folderId);

            var changedIds = remoteNotes
                .Where(n =>
                    !cacheById.TryGetValue(n.Id, out var existing) || existing.ETag != n.ETag
                )
                .Select(n => n.Id)
                .ToList();

            var freshPreviews = await _graphService.GetPreviewsAsync(changedIds);

            var merged = remoteNotes
                .Select(n => new NoteCacheEntry(
                    n.Id,
                    n.Name,
                    n.ETag,
                    n.LastModifiedUtc,
                    freshPreviews.TryGetValue(n.Id, out var preview) ? preview
                        : cacheById.TryGetValue(n.Id, out var existing) ? existing.Preview
                        : "",
                    n.Size
                ))
                .OrderBy(e => e.Name, StringComparer.OrdinalIgnoreCase)
                .ToList();

            NoteCacheStore.Save(merged);
            BindItems(merged);
        }
        catch (Exception ex)
        {
            Toast.MakeText(this, ex.Message, ToastLength.Long)?.Show();
        }
        finally
        {
            _progressBar.Visibility = ViewStates.Gone;
        }
    }

    private void OnNoteTapped(NoteCacheEntry entry)
    {
        var intent = new Intent(this, typeof(NoteViewActivity));
        intent.PutExtra(NoteViewActivity.ExtraNoteId, entry.Id);
        intent.PutExtra(NoteViewActivity.ExtraNoteName, entry.Name);
        intent.PutExtra(NoteViewActivity.ExtraNotePreview, entry.Preview);
        StartActivity(intent);
    }

    private void BindItems(List<NoteCacheEntry> entries)
    {
        _adapter.SetItems(entries);

        // Only force scroll-to-bottom on the initial (cache-empty) load; don't yank the
        // scroll position if the user is already reading older notes when a refresh lands.
        if (!_hasScrolledToBottom && entries.Count > 0)
        {
            _recyclerView.ScrollToPosition(entries.Count - 1);
            _hasScrolledToBottom = true;
        }
    }
}