/** * @file HomeView.vue * @description Main view component for the
application's home page. Handles user identity, feed management, * and
interaction with various dialogs and components. Implements infinite scrolling
for activity feed * and manages user registration status. * * @author Matthew
Raymer * @version 1.0.0 */

<template>
  <QuickNav selected="Home" />
  <TopMessage />

  <!-- CONTENT -->
  <section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
    <h1 id="ViewHeading" class="text-4xl text-center font-light mb-8">
      {{ AppString.APP_NAME }}
    </h1>

    <OnboardingDialog ref="onboardingDialog" />

    <!--
    prompt to install notifications with notificationsSupported, which we're making an advanced
    feature
    -->
    <div class="mb-8 mt-8">
      <div
        v-if="false"
        class="bg-amber-200 rounded-md overflow-hidden text-center px-4 py-3 mb-4"
      >
        <p style="display: inline; align-items: center">
          This currently doesn't support notifications, so let's fix that.
          <br />
          <!-- Note that that exact verbiage shows in the help. -->

          <span v-if="userAgentInfo.getOS().name === 'iOS'">
            Tap on "Share"<img
              src="../assets/help/apple-share-icon.svg"
              alt="Apple 'share' icon"
              width="30"
              style="display: inline; margin: 0 5px; vertical-align: middle"
            />and then "Add to Home Screen"
            <font-awesome icon="square-plus" title="Apple 'Add' icon" />
            and go click on that new app.
          </span>
          <span
            v-else-if="userAgentInfo.getBrowser()?.name?.startsWith('Chrome')"
          >
            You should see a prompt to install, or you can click on the
            top-right dots
            <font-awesome icon="ellipsis-vertical" title="vertical ellipsis" />
            /> and then "Install"<img
              src="../assets/help/install-android-chrome.png"
              alt="Android 'install' icon"
              width="30"
              style="display: inline; margin: 0 5px; vertical-align: middle"
            />
            and go use that app. If you already did these steps, reload this app
            so that it is fully detected.
          </span>
          <span v-else>
            Try
            <a href="https://www.google.com/chrome/" class="text-blue-500"
              >Google Chrome</a
            >
            or look for a way to install as an app from this browser.
          </span>
        </p>
      </div>
    </div>

    <div v-if="showShortcutBvc" class="mb-4">
      <router-link
        :to="{ name: 'quick-action-bvc' }"
        class="block text-center text-md font-bold bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white mt-2 px-2 py-3 rounded-md"
      >
        Bountiful Voluntaryist Community Actions
      </router-link>
    </div>

    <div class="mb-8">
      <div v-if="isCreatingIdentifier">
        <p class="text-slate-500 text-center italic mt-4 mb-4">
          <font-awesome icon="spinner" class="fa-spin-pulse" />
          Loading&hellip;
        </p>
      </div>

      <div v-else>
        <!-- !isCreatingIdentifier -->
        <!-- 
        They should have an identifier, even if it's an auto-generated one that they'll never use.
        -->
        <div class="mb-4">
          <div
            v-if="!isRegistered"
            id="noticeSomeoneMustRegisterYou"
            class="bg-amber-200 rounded-md overflow-hidden text-center px-4 py-3 mb-4"
          >
            <!-- !isCreatingIdentifier && !isRegistered -->
            To share, someone must register you.
            <div class="block text-center">
              <button
                class="text-md font-bold bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white mt-2 px-2 py-3 rounded-md"
                @click="showNameThenIdDialog()"
              >
                Show them {{ PASSKEYS_ENABLED ? "default" : "your" }} identifier
                info
              </button>
            </div>
            <UserNameDialog ref="userNameDialog" />
            <div v-if="PASSKEYS_ENABLED" class="flex justify-end w-full">
              <router-link
                :to="{ name: 'start' }"
                class="block text-right text-md font-bold bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white mt-2 px-2 py-3 rounded-md"
              >
                See all your options first
              </router-link>
            </div>
          </div>

          <div v-else id="sectionRecordSomethingGiven">
            <!-- !isCreatingIdentifier && isRegistered -->

            <!-- show the actions for recognizing a give -->
            <div class="flex">
              <h2 class="text-xl font-bold">What have you seen someone do?</h2>
              <button
                class="ml-2 block text-xs text-center bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1 rounded-md"
                @click="openGiftedPrompts()"
              >
                <font-awesome icon="lightbulb" class="fa-fw" />
              </button>
            </div>

            <ul
              class="grid grid-cols-4 sm:grid-cols-5 md:grid-cols-6 gap-x-3 gap-y-5 text-center mt-4"
            >
              <li @click="openDialog()">
                <img
                  src="../assets/blank-square.svg"
                  class="mx-auto border border-blue-500 rounded-md mb-1 cursor-pointer"
                />
                <h3
                  class="text-xs text-blue-500 italic font-medium text-ellipsis whitespace-nowrap overflow-hidden cursor-pointer"
                >
                  Unnamed/Unknown
                </h3>
              </li>
              <li v-if="allContacts.length === 0" class="text-sm">
                (Add friends to see more people worthy of recognition.)
              </li>
              <li
                v-for="contact in allContacts.slice(0, 6)"
                :key="contact.did"
                @click="openDialog(contact)"
              >
                <EntityIcon
                  :contact="contact"
                  :icon-size="64"
                  class="mx-auto border border-blue-500 rounded-md mb-1 cursor-pointer"
                />
                <h3
                  class="text-xs text-blue-500 font-medium text-ellipsis whitespace-nowrap overflow-hidden cursor-pointer"
                >
                  {{ contact.name || contact.did }}
                </h3>
              </li>
              <li>
                <router-link
                  v-if="allContacts.length >= 6"
                  :to="{ name: 'contact-gift' }"
                  class="flex align-bottom text-xs text-blue-500 mt-12 cursor-pointer"
                >
                  ... or someone else...
                </router-link>
              </li>
            </ul>
          </div>
        </div>
      </div>
    </div>

    <GiftedDialog ref="customDialog" />
    <GiftedPrompts ref="giftedPrompts" />
    <FeedFilters ref="feedFilters" />

    <div class="relative">
      <button
        v-if="isRegistered"
        class="absolute right-6 bottom-0 transform translate-y-1/2 text-center text-4xl leading-none bg-green-600 text-white w-14 py-2.5 rounded-full"
        @click="openDialog()"
      >
        <font-awesome icon="plus" class="fa-fw" />
      </button>
    </div>

    <!-- Results List -->
    <div class="mt-4 mb-4">
      <div class="flex items-center mb-4">
        <h2 class="text-xl font-bold flex items-center gap-4">
          Latest Activity
          <button
            v-if="resultsAreFiltered()"
            class="bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] px-3 py-1.5 rounded-md text-xs text-white"
            @click="openFeedFilters()"
          >
            <font-awesome icon="filter" class="fa-fw" />
          </button>
          <button
            v-else
            class="bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] px-3 py-1.5 rounded-md text-xs text-white"
            @click="openFeedFilters()"
          >
            <font-awesome icon="filter" class="fa-fw" />
          </button>
        </h2>
      </div>

      <div
        class="border-t p-2 border-slate-300"
        @click="goToActivityToUserPage()"
      >
        <div class="flex justify-center">
          <div
            v-if="numNewOffersToUser"
            class="bg-gradient-to-b from-green-400 to-green-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] m-1 px-4 py-4 rounded-md text-white"
          >
            <span
              class="block text-center text-6xl"
              data-testId="newDirectOffersActivityNumber"
            >
              {{ numNewOffersToUser }}{{ newOffersToUserHitLimit ? "+" : "" }}
            </span>
            <p class="text-center">
              new offer{{ numNewOffersToUser === 1 ? "" : "s" }} to you
            </p>
          </div>
          <div
            v-if="numNewOffersToUserProjects"
            class="bg-gradient-to-b from-green-400 to-green-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] m-1 px-4 py-4 rounded-md text-white"
          >
            <span
              class="block text-center text-6xl"
              data-testId="newOffersToUserProjectsActivityNumber"
            >
              {{ numNewOffersToUserProjects
              }}{{ newOffersToUserProjectsHitLimit ? "+" : "" }}
            </span>
            <p class="text-center">
              new offer{{ numNewOffersToUserProjects === 1 ? "" : "s" }} to your
              projects
            </p>
          </div>
        </div>
        <div class="flex justify-end mt-2">
          <button class="text-blue-500">View All New Activity For You</button>
        </div>
      </div>

      <InfiniteScroll @reached-bottom="loadMoreGives">
        <ul id="listLatestActivity" class="space-y-4">
          <ActivityListItem
            v-for="record in feedData"
            :key="record.jwtId"
            :record="record"
            :last-viewed-claim-id="feedLastViewedClaimId"
            :is-registered="isRegistered"
            :active-did="activeDid"
            :confirmer-id-list="record.confirmerIdList"
            @load-claim="onClickLoadClaim"
            @view-image="openImageViewer"
            @cache-image="cacheImageData"
            @confirm-claim="confirmClaim"
          />
        </ul>
      </InfiniteScroll>
      <div v-if="isFeedLoading">
        <p class="text-slate-500 text-center italic mt-4 mb-4">
          <font-awesome icon="spinner" class="fa-spin-pulse" /> Loading&hellip;
        </p>
      </div>
      <div v-if="!isFeedLoading && feedData.length === 0">
        <p class="text-slate-500 text-center italic mt-4 mb-4">
          No claims match your filters.
        </p>
      </div>
    </div>
  </section>

  <ChoiceButtonDialog ref="choiceButtonDialog" />

  <ImageViewer
    v-model:is-open="isImageViewerOpen"
    :image-url="selectedImage"
    :image-data="selectedImageData"
  />
</template>

<script lang="ts">
import { UAParser } from "ua-parser-js";
import { Component, Vue } from "vue-facing-decorator";
import { Router } from "vue-router";
import { Capacitor } from "@capacitor/core";

//import App from "../App.vue";
import EntityIcon from "../components/EntityIcon.vue";
import GiftedDialog from "../components/GiftedDialog.vue";
import GiftedPrompts from "../components/GiftedPrompts.vue";
import FeedFilters from "../components/FeedFilters.vue";
import InfiniteScroll from "../components/InfiniteScroll.vue";
import OnboardingDialog from "../components/OnboardingDialog.vue";
import QuickNav from "../components/QuickNav.vue";
import TopMessage from "../components/TopMessage.vue";
import UserNameDialog from "../components/UserNameDialog.vue";
import ChoiceButtonDialog from "../components/ChoiceButtonDialog.vue";
import ImageViewer from "../components/ImageViewer.vue";
import ActivityListItem from "../components/ActivityListItem.vue";
import {
  AppString,
  NotificationIface,
  PASSKEYS_ENABLED,
  USE_DEXIE_DB,
} from "../constants/app";
import {
  db,
  logConsoleAndDb,
  retrieveSettingsForActiveAccount,
  updateAccountSettings,
} from "../db/index";
import { Contact } from "../db/tables/contacts";
import {
  BoundingBox,
  checkIsAnyFeedFilterOn,
  MASTER_SETTINGS_KEY,
} from "../db/tables/settings";
import * as databaseUtil from "../db/databaseUtil";
import {
  contactForDid,
  containsNonHiddenDid,
  didInfoForContact,
  fetchEndorserRateLimits,
  getHeaders,
  getNewOffersToUser,
  getNewOffersToUserProjects,
  getPlanFromCache,
} from "../libs/endorserServer";
import {
  generateSaveAndActivateIdentity,
  retrieveAccountDids,
  GiverReceiverInputInfo,
  OnboardPage,
} from "../libs/util";
import { GiveSummaryRecord } from "../interfaces/records";
import * as serverUtil from "../libs/endorserServer";
import { logger } from "../utils/logger";
import { GiveRecordWithContactInfo } from "../interfaces/give";
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";

interface Claim {
  claim?: Claim; // For nested claims in Verifiable Credentials
  agent?: {
    identifier?: string;
    did?: string;
  };
  recipient?: {
    identifier?: string;
    did?: string;
  };
  provider?:
    | {
        identifier?: string;
      }
    | Array<{ identifier?: string }>;
  object?: {
    amountOfThisGood?: number;
    unitCode?: string;
  };
  description?: string;
  image?: string;
}

interface FulfillsPlan {
  locLat?: number;
  locLon?: number;
  name?: string;
}

interface Provider {
  identifier?: string;
}

interface ProvidedByPlan {
  name?: string;
}

interface FeedError {
  userMessage?: string;
}

/**
 * HomeView Component
 *
 * Main view component that handles:
 * 1. User identity and registration management
 * 2. Activity feed with infinite scrolling
 * 3. Contact management and display
 * 4. Gift/claim creation and viewing
 * 5. Feed filtering and settings
 *
 * Template Usage:
 * ```vue
 * <HomeView>
 *   <!-- Content is managed internally -->
 * </HomeView>
 * ```
 *
 * Component Dependencies:
 * - QuickNav: Navigation component
 * - TopMessage: Message display component
 * - OnboardingDialog: User onboarding flow
 * - GiftedDialog: Gift creation interface
 * - FeedFilters: Feed filtering options
 * - InfiniteScroll: Infinite scrolling functionality
 * - ActivityListItem: Individual activity display
 */
@Component({
  components: {
    EntityIcon,
    FeedFilters,
    GiftedDialog,
    GiftedPrompts,
    InfiniteScroll,
    OnboardingDialog,
    ChoiceButtonDialog,
    QuickNav,
    TopMessage,
    UserNameDialog,
    ImageViewer,
    ActivityListItem,
  },
})
export default class HomeView extends Vue {
  $notify!: (notification: NotificationIface, timeout?: number) => void;
  $router!: Router;

  AppString = AppString;
  PASSKEYS_ENABLED = PASSKEYS_ENABLED;

  activeDid = "";
  allContacts: Array<Contact> = [];
  allMyDids: Array<string> = [];
  apiServer = "";
  feedData: GiveRecordWithContactInfo[] = [];
  feedPreviousOldestId?: string;
  feedLastViewedClaimId?: string;
  givenName = "";
  isAnyFeedFilterOn = false;
  isCreatingIdentifier = false;
  isFeedFilteredByVisible = false;
  isFeedFilteredByNearby = false;
  isFeedLoading = true;
  isRegistered = false;
  lastAckedOfferToUserJwtId?: string; // the last JWT ID for offer-to-user that they've acknowledged seeing
  lastAckedOfferToUserProjectsJwtId?: string; // the last JWT ID for offers-to-user's-projects that they've acknowledged seeing
  newOffersToUserHitLimit: boolean = false;
  newOffersToUserProjectsHitLimit: boolean = false;
  numNewOffersToUser: number = 0; // number of new offers-to-user
  numNewOffersToUserProjects: number = 0; // number of new offers-to-user's-projects
  searchBoxes: Array<{
    name: string;
    bbox: BoundingBox;
  }> = [];
  showShortcutBvc = false;
  userAgentInfo = new UAParser(); // see https://docs.uaparser.js.org/v2/api/ua-parser-js/get-os.html
  selectedImage = "";
  selectedImageData: Blob | null = null;
  isImageViewerOpen = false;
  imageCache: Map<string, Blob | null> = new Map();

  /**
   * Initializes the component on mount
   * Sequence:
   * 1. Initialize identity (create if needed)
   * 2. Load user settings
   * 3. Load contacts
   * 4. Check registration status
   * 5. Load feed data
   * 6. Load new offers
   * 7. Check onboarding status
   *
   * @internal
   * Called automatically by Vue lifecycle system
   */
  async mounted() {
    try {
      await this.initializeIdentity();
      await this.loadSettings();
      await this.loadContacts();
      await this.checkRegistrationStatus();
      await this.loadFeedData();
      await this.loadNewOffers();
      await this.checkOnboarding();
    } catch (err: unknown) {
      this.handleError(err);
    }
  }

  /**
   * Initializes user identity
   * - Retrieves existing DIDs
   * - Creates new DID if none exists
   * - Loads user settings and contacts
   * - Checks registration status
   *
   * @internal
   * Called by mounted()
   * @throws Logs error if DID retrieval fails
   */
  private async initializeIdentity() {
    try {
      // Retrieve DIDs with better error handling
      try {
        this.allMyDids = await retrieveAccountDids();
        logConsoleAndDb(`[HomeView] Retrieved ${this.allMyDids.length} DIDs`);
      } catch (error) {
        logConsoleAndDb(`[HomeView] Failed to retrieve DIDs: ${error}`, true);
        throw new Error(
          "Failed to load existing identities. Please try restarting the app.",
        );
      }

      // Create new DID if needed
      if (this.allMyDids.length === 0) {
        try {
          this.isCreatingIdentifier = true;
          const newDid = await generateSaveAndActivateIdentity();
          this.isCreatingIdentifier = false;
          this.allMyDids = [newDid];
          logConsoleAndDb(`[HomeView] Created new identity: ${newDid}`);
        } catch (error) {
          this.isCreatingIdentifier = false;
          logConsoleAndDb(
            `[HomeView] Failed to create new identity: ${error}`,
            true,
          );
          throw new Error("Failed to create new identity. Please try again.");
        }
      }

      // Load settings with better error context
      let settings;
      try {
        settings = await databaseUtil.retrieveSettingsForActiveAccount();
        if (USE_DEXIE_DB) {
          settings = await retrieveSettingsForActiveAccount();
        }
        logConsoleAndDb(
          `[HomeView] Retrieved settings for ${settings.activeDid || "no active DID"}`,
        );
      } catch (error) {
        logConsoleAndDb(
          `[HomeView] Failed to retrieve settings: ${error}`,
          true,
        );
        throw new Error(
          "Failed to load user settings. Some features may be limited.",
        );
      }

      // Update component state
      this.apiServer = settings.apiServer || "";
      this.activeDid = settings.activeDid || "";

      // Load contacts with graceful fallback
      try {
        const platformService = PlatformServiceFactory.getInstance();
        const dbContacts = await platformService.dbQuery(
          "SELECT * FROM contacts",
        );
        this.allContacts = databaseUtil.mapQueryResultToValues(
          dbContacts,
        ) as Contact[];
        if (USE_DEXIE_DB) {
          this.allContacts = await db.contacts.toArray();
        }
        logConsoleAndDb(
          `[HomeView] Retrieved ${this.allContacts.length} contacts`,
        );
      } catch (error) {
        logConsoleAndDb(
          `[HomeView] Failed to retrieve contacts: ${error}`,
          true,
        );
        this.allContacts = []; // Ensure we have a valid empty array
        this.$notify(
          {
            group: "alert",
            type: "warning",
            title: "Contact Loading Issue",
            text: "Some contact information may be unavailable.",
          },
          5000,
        );
      }

      // Update remaining settings
      this.feedLastViewedClaimId = settings.lastViewedClaimId;
      this.givenName = settings.firstName || "";
      this.isFeedFilteredByVisible = !!settings.filterFeedByVisible;
      this.isFeedFilteredByNearby = !!settings.filterFeedByNearby;
      this.isRegistered = !!settings.isRegistered;
      this.lastAckedOfferToUserJwtId = settings.lastAckedOfferToUserJwtId;
      this.lastAckedOfferToUserProjectsJwtId =
        settings.lastAckedOfferToUserProjectsJwtId;
      this.searchBoxes = settings.searchBoxes || [];
      this.showShortcutBvc = !!settings.showShortcutBvc;
      this.isAnyFeedFilterOn = checkIsAnyFeedFilterOn(settings);

      // Check onboarding status
      if (!settings.finishedOnboarding) {
        (this.$refs.onboardingDialog as OnboardingDialog).open(
          OnboardPage.Home,
        );
      }

      // Check registration status if needed
      if (!this.isRegistered && this.activeDid) {
        try {
          const resp = await fetchEndorserRateLimits(
            this.apiServer,
            this.axios,
            this.activeDid,
          );
          if (resp.status === 200) {
            await databaseUtil.updateDidSpecificSettings(this.activeDid, {
              isRegistered: true,
              ...(await databaseUtil.retrieveSettingsForActiveAccount()),
            });
            if (USE_DEXIE_DB) {
              await updateAccountSettings(this.activeDid, {
                isRegistered: true,
                ...(await retrieveSettingsForActiveAccount()),
              });
            }
            this.isRegistered = true;
            logConsoleAndDb(
              `[HomeView] User ${this.activeDid} is now registered`,
            );
          }
        } catch (error) {
          logConsoleAndDb(
            `[HomeView] Registration check failed: ${error}`,
            true,
          );
          // Continue as unregistered - this is expected for new users
        }
      }

      // Initialize feed and offers
      try {
        // Start feed update in background
        this.updateAllFeed().catch((error) => {
          logConsoleAndDb(
            `[HomeView] Background feed update failed: ${error}`,
            true,
          );
        });

        // Load new offers if we have an active DID
        if (this.activeDid) {
          const [offersToUser, offersToProjects] = await Promise.all([
            getNewOffersToUser(
              this.axios,
              this.apiServer,
              this.activeDid,
              this.lastAckedOfferToUserJwtId,
            ),
            getNewOffersToUserProjects(
              this.axios,
              this.apiServer,
              this.activeDid,
              this.lastAckedOfferToUserProjectsJwtId,
            ),
          ]);

          this.numNewOffersToUser = offersToUser.data.length;
          this.newOffersToUserHitLimit = offersToUser.hitLimit;
          this.numNewOffersToUserProjects = offersToProjects.data.length;
          this.newOffersToUserProjectsHitLimit = offersToProjects.hitLimit;

          logConsoleAndDb(
            `[HomeView] Retrieved ${this.numNewOffersToUser} user offers and ` +
              `${this.numNewOffersToUserProjects} project offers`,
          );
        }
      } catch (error) {
        logConsoleAndDb(
          `[HomeView] Failed to initialize feed/offers: ${error}`,
          true,
        );
        // Don't throw - we can continue with empty feed
        this.$notify(
          {
            group: "alert",
            type: "warning",
            title: "Feed Loading Issue",
            text: "Some feed data may be unavailable. Pull to refresh.",
          },
          5000,
        );
      }
    } catch (error) {
      this.handleError(error);
      throw error; // Re-throw to be caught by mounted()
    }
  }

  /**
   * Loads user settings from storage
   * Sets component state for:
   * - API server
   * - Active DID
   * - Feed filters and view settings
   * - Registration status
   * - Notification acknowledgments
   *
   * @internal
   * Called by mounted() and reloadFeedOnChange()
   */
  private async loadSettings() {
    let settings = await databaseUtil.retrieveSettingsForActiveAccount();
    if (USE_DEXIE_DB) {
      settings = await retrieveSettingsForActiveAccount();
    }
    this.apiServer = settings.apiServer || "";
    this.activeDid = settings.activeDid || "";
    this.feedLastViewedClaimId = settings.lastViewedClaimId;
    this.givenName = settings.firstName || "";
    this.isFeedFilteredByVisible = !!settings.filterFeedByVisible;
    this.isFeedFilteredByNearby = !!settings.filterFeedByNearby;
    this.isRegistered = !!settings.isRegistered;
    this.lastAckedOfferToUserJwtId = settings.lastAckedOfferToUserJwtId;
    this.lastAckedOfferToUserProjectsJwtId =
      settings.lastAckedOfferToUserProjectsJwtId;
    this.searchBoxes = settings.searchBoxes || [];
    this.showShortcutBvc = !!settings.showShortcutBvc;
    this.isAnyFeedFilterOn = checkIsAnyFeedFilterOn(settings);
  }

  /**
   * Loads user contacts from database
   * Used for displaying contact info in feed and actions
   *
   * @internal
   * Called by mounted() and initializeIdentity()
   */
  private async loadContacts() {
    const platformService = PlatformServiceFactory.getInstance();
    const dbContacts = await platformService.dbQuery("SELECT * FROM contacts");
    this.allContacts = databaseUtil.mapQueryResultToValues(
      dbContacts,
    ) as unknown as Contact[];
    if (USE_DEXIE_DB) {
      this.allContacts = await db.contacts.toArray();
    }
  }

  /**
   * Verifies user registration status with endorser service
   * - Checks if unregistered user can access API
   * - Updates registration status if successful
   * - Preserves unregistered state on failure
   *
   * @internal
   * Called by mounted() and initializeIdentity()
   */
  private async checkRegistrationStatus() {
    if (!this.isRegistered && this.activeDid) {
      try {
        const resp = await fetchEndorserRateLimits(
          this.apiServer,
          this.axios,
          this.activeDid,
        );
        if (resp.status === 200) {
          let settings = await databaseUtil.retrieveSettingsForActiveAccount();
          if (USE_DEXIE_DB) {
            settings = await retrieveSettingsForActiveAccount();
          }
          await databaseUtil.updateDidSpecificSettings(this.activeDid, {
            apiServer: this.apiServer,
            isRegistered: true,
            ...settings,
          });
          if (USE_DEXIE_DB) {
            await updateAccountSettings(this.activeDid, {
              apiServer: this.apiServer,
              isRegistered: true,
              ...settings,
            });
          }
          this.isRegistered = true;
        }
      } catch (e) {
        // ignore the error... just keep us unregistered
      }
    }
  }

  /**
   * Initializes feed data
   * Triggers updateAllFeed() to populate activity feed
   *
   * @internal
   * Called by mounted()
   */
  private async loadFeedData() {
    await this.updateAllFeed();
  }

  /**
   * Loads new offers for user and their projects
   * Updates:
   * - Number of new direct offers
   * - Number of new project offers
   * - Rate limit status for both
   *
   * @internal
   * Called by mounted() and initializeIdentity()
   * @requires Active DID
   */
  private async loadNewOffers() {
    if (this.activeDid) {
      const offersToUserData = await getNewOffersToUser(
        this.axios,
        this.apiServer,
        this.activeDid,
        this.lastAckedOfferToUserJwtId,
      );
      this.numNewOffersToUser = offersToUserData.data.length;
      this.newOffersToUserHitLimit = offersToUserData.hitLimit;

      const offersToUserProjects = await getNewOffersToUserProjects(
        this.axios,
        this.apiServer,
        this.activeDid,
        this.lastAckedOfferToUserProjectsJwtId,
      );
      this.numNewOffersToUserProjects = offersToUserProjects.data.length;
      this.newOffersToUserProjectsHitLimit = offersToUserProjects.hitLimit;
    }
  }

  /**
   * Checks if user needs onboarding
   * Opens onboarding dialog if not completed
   *
   * @internal
   * Called by mounted()
   */
  private async checkOnboarding() {
    let settings = await databaseUtil.retrieveSettingsForActiveAccount();
    if (USE_DEXIE_DB) {
      settings = await retrieveSettingsForActiveAccount();
    }
    if (!settings.finishedOnboarding) {
      (this.$refs.onboardingDialog as OnboardingDialog).open(OnboardPage.Home);
    }
  }

  /**
   * Handles errors during initialization
   * - Logs error to console and database
   * - Displays user notification
   *
   * @internal
   * Called by mounted() and initializeIdentity()
   * @param err Error object with optional userMessage
   */
  private handleError(err: unknown) {
    const errorMessage = err instanceof Error ? err.message : String(err);
    const userMessage = (err as { userMessage?: string })?.userMessage;

    logConsoleAndDb(
      `[HomeView] Initialization error: ${errorMessage}${userMessage ? ` (${userMessage})` : ""}`,
      true,
    );

    this.$notify(
      {
        group: "alert",
        type: "danger",
        title: "Error",
        text:
          userMessage ||
          "There was an error loading your data. Please try refreshing the page.",
      },
      5000,
    );
  }

  /**
   * Checks if feed results are being filtered
   *
   * @public
   * Used in template for filter button display
   * @returns true if visible or nearby filters are active
   */
  resultsAreFiltered() {
    return this.isFeedFilteredByVisible || this.isFeedFilteredByNearby;
  }

  /**
   * Checks if browser notifications are supported
   *
   * @public
   * Used in template for notification feature detection
   * @returns true if Notification API is available
   */
  notificationsSupported() {
    return "Notification" in window;
  }

  /**
   * Reloads feed when filter settings change
   * - Updates filter states
   * - Clears existing feed data
   * - Triggers new feed load
   *
   * @public
   * Called by FeedFilters component when filters change
   */
  async reloadFeedOnChange() {
    let settings = await databaseUtil.retrieveSettingsForActiveAccount();
    if (USE_DEXIE_DB) {
      settings = await retrieveSettingsForActiveAccount();
    }
    this.isFeedFilteredByVisible = !!settings.filterFeedByVisible;
    this.isFeedFilteredByNearby = !!settings.filterFeedByNearby;
    this.isAnyFeedFilterOn = checkIsAnyFeedFilterOn(settings);

    this.feedData = [];
    this.feedPreviousOldestId = undefined;
    await this.updateAllFeed();
  }

  /**
   * Loads more feed items for infinite scroll
   *
   * @public
   * Called by InfiniteScroll component when bottom is reached
   * @param payload Boolean indicating if more items should be loaded
   */
  async loadMoreGives(payload: boolean) {
    // Since feed now loads projects along the way, it takes longer
    // and the InfiniteScroll component triggers a load before finished.
    // One alternative is to totally separate the project link loading.
    if (payload && !this.isFeedLoading) {
      await this.updateAllFeed();
    }
  }

  /**
   * Checks if coordinates fall within any search box
   *
   * @internal
   * @callGraph
   * Called by: shouldIncludeRecord()
   * Calls: None
   *
   * @chain
   * shouldIncludeRecord() -> latLongInAnySearchBox()
   *
   * @requires
   * - this.searchBoxes
   *
   * @param lat Latitude to check
   * @param long Longitude to check
   * @returns true if coordinates are within any search box
   */
  latLongInAnySearchBox(lat: number, long: number) {
    for (const boxInfo of this.searchBoxes) {
      if (
        boxInfo.bbox.westLong <= long &&
        long <= boxInfo.bbox.eastLong &&
        boxInfo.bbox.minLat <= lat &&
        lat <= boxInfo.bbox.maxLat
      ) {
        return true;
      }
    }
  }

  /**
   * Updates feed with latest activity
   *
   * @internal
   * @callGraph
   * Called by:
   * - loadMoreGives()
   * - initializeIdentity()
   * Calls:
   * - retrieveGives()
   * - processFeedResults()
   * - updateFeedLastViewedId()
   * - handleFeedError()
   *
   * @chain
   * loadMoreGives() -> updateAllFeed()
   * initializeIdentity() -> updateAllFeed()
   *
   * @requires
   * - this.apiServer
   * - this.activeDid
   * - this.feedPreviousOldestId
   *
   * @modifies
   * - this.isFeedLoading
   * - this.feedData (via processFeedResults)
   * - this.feedLastViewedClaimId (via updateFeedLastViewedId)
   */
  async updateAllFeed() {
    this.isFeedLoading = true;
    let endOfResults = true;

    try {
      const results = await this.retrieveGives(
        this.apiServer,
        this.feedPreviousOldestId,
      );
      if (results.data.length > 0) {
        endOfResults = false;
        await this.processFeedResults(results.data);
        await this.updateFeedLastViewedId(results.data);
      }
    } catch (e) {
      this.handleFeedError(e);
    }

    if (this.feedData.length === 0 && !endOfResults) {
      await this.updateAllFeed();
    }

    this.isFeedLoading = false;
  }

  /**
   * Processes feed results and adds them to feedData
   *
   * @internal
   * @callGraph
   * Called by: updateAllFeed()
   * Calls: processRecord()
   *
   * @chain
   * updateAllFeed() -> processFeedResults()
   *
   * @requires
   * - this.feedData
   * - this.feedPreviousOldestId
   *
   * @modifies
   * - this.feedData
   * - this.feedPreviousOldestId
   *
   * @param records Array of feed records to process
   */
  private async processFeedResults(records: GiveSummaryRecord[]) {
    for (const record of records) {
      const processedRecord = await this.processRecord(record);
      if (processedRecord) {
        this.feedData.push(processedRecord);
      }
    }
    this.feedPreviousOldestId = records[records.length - 1].jwtId;
  }

  /**
   * Processes a single record and returns it if it passes filters
   *
   * @internal
   * @callGraph
   * Called by: processFeedResults()
   * Calls:
   * - extractClaim()
   * - extractGiverDid()
   * - extractRecipientDid()
   * - getFulfillsPlan()
   * - shouldIncludeRecord()
   * - extractProvider()
   * - getProvidedByPlan()
   * - createFeedRecord()
   *
   * @chain
   * updateAllFeed() -> processFeedResults() -> processRecord()
   *
   * @requires
   * - this.isAnyFeedFilterOn
   * - this.isFeedFilteredByVisible
   * - this.isFeedFilteredByNearby
   * - this.activeDid
   * - this.allContacts
   *
   * @modifies
   * - this.feedData (via createFeedRecord)
   *
   * @param record The record to process
   * @returns Processed record with contact info if it passes filters, null otherwise
   */
  private async processRecord(
    record: GiveSummaryRecord,
  ): Promise<GiveRecordWithContactInfo | null> {
    const claim = this.extractClaim(record);
    const giverDid = this.extractGiverDid(claim);
    const recipientDid = this.extractRecipientDid(claim);

    const fulfillsPlan = await this.getFulfillsPlan(record);
    if (!this.shouldIncludeRecord(record, fulfillsPlan)) {
      return null;
    }

    const provider = this.extractProvider(claim);
    const providedByPlan = await this.getProvidedByPlan(provider);

    return this.createFeedRecord(
      record,
      claim,
      giverDid,
      recipientDid,
      provider,
      fulfillsPlan,
      providedByPlan,
    );
  }

  /**
   * Extracts claim from record, handling both direct and wrapped claims
   *
   * @internal
   * @callGraph
   * Called by: processRecord()
   * Calls: None
   *
   * @chain
   * processRecord() -> extractClaim()
   *
   * @requires
   * - record.fullClaim
   *
   * @param record The record containing the claim
   * @returns The extracted claim object
   */
  private extractClaim(record: GiveSummaryRecord) {
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    return (record.fullClaim as any).claim || record.fullClaim;
  }

  /**
   * Extracts giver DID from claim
   *
   * @internal
   * @callGraph
   * Called by: processRecord()
   * Calls: None
   *
   * @chain
   * processRecord() -> extractGiverDid()
   *
   * @requires
   * - claim.agent
   *
   * @param claim The claim object containing giver information
   * @returns The giver's DID
   */
  private extractGiverDid(claim: Claim) {
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    return claim.agent?.identifier || (claim.agent as any)?.did;
  }

  /**
   * Extracts recipient DID from claim
   *
   * @internal
   * Called by processRecord()
   */
  private extractRecipientDid(claim: Claim) {
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    return claim.recipient?.identifier || (claim.recipient as any)?.did;
  }

  /**
   * Gets fulfills plan from cache
   *
   * @internal
   * @callGraph
   * Called by: processRecord()
   * Calls: getPlanFromCache()
   *
   * @chain
   * processRecord() -> getFulfillsPlan()
   *
   * @requires
   * - this.axios
   * - this.apiServer
   * - this.activeDid
   *
   * @param record The record containing the plan handle ID
   * @returns The fulfills plan object
   */
  private async getFulfillsPlan(record: GiveSummaryRecord) {
    return await getPlanFromCache(
      record.fulfillsPlanHandleId,
      this.axios,
      this.apiServer,
      this.activeDid,
    );
  }

  /**
   * Checks if record should be included based on filters
   *
   * @internal
   * @callGraph
   * Called by: processRecord()
   * Calls:
   * - containsNonHiddenDid()
   * - latLongInAnySearchBox()
   *
   * @chain
   * processRecord() -> shouldIncludeRecord()
   *
   * @requires
   * - this.isAnyFeedFilterOn
   * - this.isFeedFilteredByVisible
   * - this.isFeedFilteredByNearby
   * - this.searchBoxes
   *
   * @param record The record to check
   * @param fulfillsPlan The fulfills plan object
   * @returns true if record should be included based on filters
   */
  private shouldIncludeRecord(
    record: GiveSummaryRecord,
    fulfillsPlan?: FulfillsPlan,
  ): boolean {
    if (!this.isAnyFeedFilterOn) {
      return true;
    }

    let anyMatch = false;
    if (this.isFeedFilteredByVisible && containsNonHiddenDid(record)) {
      anyMatch = true;
    }

    if (
      !anyMatch &&
      this.isFeedFilteredByNearby &&
      record.fulfillsPlanHandleId
    ) {
      if (fulfillsPlan?.locLat && fulfillsPlan?.locLon) {
        anyMatch =
          this.latLongInAnySearchBox(
            fulfillsPlan.locLat,
            fulfillsPlan.locLon,
          ) ?? false;
      }
    }

    return anyMatch;
  }

  /**
   * Extracts provider from claim
   *
   * @internal
   * Called by processRecord()
   */
  private extractProvider(claim: Claim): Provider | undefined {
    return Array.isArray(claim.provider) ? claim.provider[0] : claim.provider;
  }

  /**
   * Gets provided by plan from cache
   *
   * @internal
   * Called by processRecord()
   */
  private async getProvidedByPlan(provider: Provider | undefined) {
    return await getPlanFromCache(
      provider?.identifier as string,
      this.axios,
      this.apiServer,
      this.activeDid,
    );
  }

  /**
   * Creates a feed record with contact info
   *
   * @internal
   * @callGraph
   * Called by: processRecord()
   * Calls:
   * - didInfoForContact()
   * - contactForDid()
   *
   * @chain
   * processRecord() -> createFeedRecord()
   *
   * @requires
   * - this.activeDid
   * - this.allContacts
   * - this.allMyDids
   *
   * @param record The base record
   * @param claim The claim object
   * @param giverDid The giver's DID
   * @param recipientDid The recipient's DID
   * @param provider The provider object
   * @param fulfillsPlan The fulfills plan object
   * @param providedByPlan The provided by plan object
   * @returns A feed record with contact information
   */
  private createFeedRecord(
    record: GiveSummaryRecord,
    claim: Claim,
    giverDid: string,
    recipientDid: string,
    provider: Provider | undefined,
    fulfillsPlan?: FulfillsPlan,
    providedByPlan?: ProvidedByPlan,
  ): GiveRecordWithContactInfo {
    return {
      ...record,
      jwtId: record.jwtId,
      fullClaim: record.fullClaim,
      description: record.description || "",
      handleId: record.handleId,
      issuerDid: record.issuerDid,
      fulfillsPlanHandleId: record.fulfillsPlanHandleId,
      giver: didInfoForContact(
        giverDid,
        this.activeDid,
        contactForDid(giverDid, this.allContacts),
        this.allMyDids,
      ),
      image: claim.image,
      issuer: didInfoForContact(
        record.issuerDid,
        this.activeDid,
        contactForDid(record.issuerDid, this.allContacts),
        this.allMyDids,
      ),
      providerPlanHandleId: provider?.identifier as string,
      providerPlanName: providedByPlan?.name as string,
      recipientProjectName: fulfillsPlan?.name as string,
      receiver: didInfoForContact(
        recipientDid,
        this.activeDid,
        contactForDid(recipientDid, this.allContacts),
        this.allMyDids,
      ),
    } as GiveRecordWithContactInfo;
  }

  /**
   * Updates the last viewed claim ID in settings
   *
   * @internal
   * Called by updateAllFeed()
   */
  private async updateFeedLastViewedId(records: GiveSummaryRecord[]) {
    if (
      this.feedLastViewedClaimId == null ||
      this.feedLastViewedClaimId < records[0].jwtId
    ) {
      await databaseUtil.updateDefaultSettings({
        lastViewedClaimId: records[0].jwtId,
      });
      if (USE_DEXIE_DB) {
        await db.open();
        await db.settings.update(MASTER_SETTINGS_KEY, {
          lastViewedClaimId: records[0].jwtId,
        });
      }
    }
  }

  /**
   * Handles feed error and shows notification
   *
   * @internal
   * Called by updateAllFeed()
   */
  private handleFeedError(e: unknown) {
    logger.error("Error with feed load:", e);
    this.$notify(
      {
        group: "alert",
        type: "danger",
        title: "Feed Error",
        text:
          (e as FeedError)?.userMessage ||
          "There was an error retrieving feed data.",
      },
      -1,
    );
  }

  /**
   * Retrieve claims in reverse chronological order
   *
   * @internal
   * Called by updateAllFeed()
   * @param endorserApiServer API server URL
   * @param beforeId OptioCalled by updateAllFeed()nal ID to fetch earlier results
   * @returns claims in reverse chronological order
   */
  async retrieveGives(endorserApiServer: string, beforeId?: string) {
    const beforeQuery = beforeId == null ? "" : "&beforeId=" + beforeId;
    const doNotShowErrorAgain = !!beforeId; // don't show error again if we're loading more
    const headers = await getHeaders(
      this.activeDid,
      doNotShowErrorAgain ? undefined : this.$notify,
    );
    // retrieve headers for this user, but if an error happens then report it but proceed with the fetch with no header
    const response = await fetch(
      endorserApiServer +
        "/api/v2/report/gives?giftNotTrade=true" +
        beforeQuery,
      {
        method: "GET",
        headers: headers,
      },
    );

    if (!response.ok) {
      throw await response.text();
    }

    const results = await response.json();

    if (results.data) {
      return results;
    } else {
      throw JSON.stringify(results);
    }
  }

  /**
   * Formats gift description with giver and recipient info
   *
   * @public
   * @callGraph
   * Called by: Template
   * Calls: displayAmount()
   *
   * @chain
   * Template -> giveDescription() -> displayAmount() -> currencyShortWordForCode()
   *
   * @requires
   * - giveRecord.fullClaim
   * - giveRecord.giver
   * - giveRecord.receiver
   * - giveRecord.recipientProjectName
   * - giveRecord.providerPlanName
   *
   * @param giveRecord Record containing gift information
   * @returns formatted description string
   */
  giveDescription(giveRecord: GiveRecordWithContactInfo) {
    // similar code is in endorser-mobile utility.ts
    // claim.claim happen for some claims wrapped in a Verifiable Credential
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    const claim = (giveRecord.fullClaim as any).claim || giveRecord.fullClaim;

    let gaveAmount = claim.object?.amountOfThisGood
      ? this.displayAmount(claim.object.unitCode, claim.object.amountOfThisGood)
      : "";
    if (claim.description) {
      if (gaveAmount) {
        gaveAmount = " (and " + gaveAmount + ")";
      }
      gaveAmount = claim.description + gaveAmount;
    }
    if (!gaveAmount) {
      gaveAmount = "something not described";
    }

    /**
     * Only show giver and/or receiver info first if they're named in your contacts.
     * - If only giver is named, show "... gave"
     * - If only receiver is named, show "... received"
     */

    const giverInfo = giveRecord.giver;
    const recipientInfo = giveRecord.receiver;

    // any specific names should be shown first
    if (giverInfo.known && recipientInfo.known) {
      // both giver and recipient are named
      return `${giverInfo.displayName} gave to ${recipientInfo.displayName}: ${gaveAmount}`;
    } else if (giverInfo.known) {
      // giver is known but recipient is not

      // show the project name if to one
      if (giveRecord.recipientProjectName) {
        return `${giverInfo.displayName} gave: ${gaveAmount} (to the project "${giveRecord.recipientProjectName}")`;
      } else {
        // it's not to a project
        return `${giverInfo.displayName} gave: ${gaveAmount} (to ${recipientInfo.displayName})`;
      }
    } else if (recipientInfo.known) {
      // recipient is known but giver is not

      // show the project name if from one
      if (giveRecord.providerPlanName) {
        return `${recipientInfo.displayName} received: ${gaveAmount} (from the project "${giveRecord.providerPlanName}")`;
      } else {
        // it's not from a project
        return `${recipientInfo.displayName} received: ${gaveAmount} (from ${giverInfo.displayName})`;
      }
    } else {
      // neither giver nor recipient are named

      // create the part in parens
      let peopleInfo = "";
      if (giveRecord.providerPlanName || giveRecord.recipientProjectName) {
        if (giveRecord.providerPlanName) {
          peopleInfo = `from the project "${giveRecord.providerPlanName}"`;
        } else {
          peopleInfo = `from ${giverInfo.displayName}`;
        }
        if (giveRecord.recipientProjectName) {
          peopleInfo += ` to the project "${giveRecord.recipientProjectName}"`;
        } else {
          peopleInfo += ` to ${recipientInfo.displayName}`;
        }
      } else {
        if (giverInfo.displayName === recipientInfo.displayName) {
          peopleInfo = `between two who are ${giverInfo.displayName}`;
        } else {
          peopleInfo = `from ${giverInfo.displayName} to ${recipientInfo.displayName}`;
        }
      }

      return gaveAmount + " (" + peopleInfo + ")";
    }
  }

  /**
   * Navigates to activity page
   *
   * @public
   * Called by template click handler
   */
  goToActivityToUserPage() {
    this.$router.push({ name: "new-activity" });
  }

  /**
   * Navigates to claim details page
   *
   * @public
   * Called by ActivityListItem component
   * @param jwtId ID of the claim to view
   */
  onClickLoadClaim(jwtId: string) {
    const route = {
      path: "/claim/" + encodeURIComponent(jwtId),
    };
    this.$router.push(route);
  }

  /**
   * Formats amount with currency code
   *
   * @internal
   * @callGraph
   * Called by: giveDescription()
   * Calls: currencyShortWordForCode()
   *
   * @chain
   * giveDescription() -> displayAmount() -> currencyShortWordForCode()
   *
   * @requires
   * - code: string (currency code)
   * - amt: number (amount to format)
   *
   * @param code Currency code
   * @param amt Amount to format
   * @returns formatted amount string
   */
  displayAmount(code: string, amt: number) {
    return "" + amt + " " + this.currencyShortWordForCode(code, amt === 1);
  }

  /**
   * Gets currency word based on code and plurality
   *
   * @internal
   * @callGraph
   * Called by: displayAmount()
   * Calls: None
   *
   * @chain
   * giveDescription() -> displayAmount() -> currencyShortWordForCode()
   *
   * @requires
   * - unitCode: string (currency code)
   * - single: boolean (whether to use singular form)
   *
   * @param unitCode Currency code
   * @param single Whether to use singular form
   * @returns formatted currency word
   */
  currencyShortWordForCode(unitCode: string, single: boolean) {
    return unitCode === "HUR" ? (single ? "hour" : "hours") : unitCode;
  }

  /**
   * Opens dialog for creating new gift/claim
   *
   * @public
   * @callGraph
   * Called by:
   * - Template
   * - openGiftedPrompts()
   * Calls: GiftedDialog.open()
   *
   * @chain
   * Template -> openDialog()
   * openGiftedPrompts() -> openDialog()
   *
   * @requires
   * - this.$refs.customDialog
   * - this.activeDid
   *
   * @param giver Optional contact info for giver
   * @param description Optional gift description
   */
  openDialog(giver?: GiverReceiverInputInfo, description?: string) {
    (this.$refs.customDialog as GiftedDialog).open(
      giver,
      {
        did: this.activeDid,
        name: "you",
      } as GiverReceiverInputInfo,
      undefined,
      "Given by " + (giver?.name || "someone not named"),
      description,
    );
  }

  /**
   * Opens prompts for gift ideas
   *
   * @public
   * @callGraph
   * Called by: Template
   * Calls: openDialog()
   *
   * @chain
   * Template -> openGiftedPrompts() -> openDialog()
   *
   * @requires
   * - this.$refs.giftedPrompts
   *
   * @param callback Function to handle selected gift info
   */
  openGiftedPrompts() {
    (this.$refs.giftedPrompts as GiftedPrompts).open((giver, description) =>
      this.openDialog(giver as GiverReceiverInputInfo, description),
    );
  }

  /**
   * Opens feed filter configuration
   *
   * @public
   * Called by template click handler
   */
  openFeedFilters() {
    (this.$refs.feedFilters as FeedFilters).open(this.reloadFeedOnChange);
  }

  /**
   * Shows toast notification to user
   *
   * @internal
   * Used for various user notifications
   * @param message Message to display
   */
  toastUser(message: string) {
    this.$notify(
      {
        group: "alert",
        type: "toast",
        title: "FYI",
        text: message,
      },
      2000,
    );
  }

  /**
   * Computes CSS classes for known person icons
   *
   * @public
   * Used in template for icon styling
   * @param known Whether the person is known
   * @returns CSS class string
   */
  computeKnownPersonIconStyleClassNames(known: boolean) {
    return known ? "text-slate-500" : "text-slate-100";
  }

  /**
   * Shows name input dialog if needed
   *
   * @public
   * @callGraph
   * Called by: Template
   * Calls:
   * - UserNameDialog.open()
   * - promptForShareMethod()
   *
   * @chain
   * Template -> showNameThenIdDialog() -> promptForShareMethod()
   *
   * @requires
   * - this.$refs.userNameDialog
   * - this.givenName
   */
  showNameThenIdDialog() {
    if (!this.givenName) {
      (this.$refs.userNameDialog as UserNameDialog).open(() => {
        this.promptForShareMethod();
      });
    } else {
      this.promptForShareMethod();
    }
  }

  /**
   * Shows dialog for sharing method selection
   *
   * @internal
   * @callGraph
   * Called by: showNameThenIdDialog()
   * Calls: ChoiceButtonDialog.open()
   *
   * @chain
   * Template -> showNameThenIdDialog() -> promptForShareMethod()
   *
   * @requires
   * - this.$refs.choiceButtonDialog
   * - this.$router
   */
  promptForShareMethod() {
    (this.$refs.choiceButtonDialog as ChoiceButtonDialog).open({
      title: "How can you share your info?",
      text: "",
      option1Text: "We are in a meeting together",
      option2Text: "We are nearby with cameras",
      option3Text: "We will share some other way",
      onOption1: () => {
        this.$router.push({ name: "onboard-meeting-list" });
      },
      onOption2: () => {
        this.handleQRCodeClick();
      },
      onOption3: () => {
        this.$router.push({ name: "share-my-contact-info" });
      },
    });
  }

  /**
   * Caches image data for sharing
   *
   * @public
   * Called by ActivityListItem component
   * @param event Event object
   * @param imageUrl URL of image to cache
   */
  async cacheImageData(event: Event, imageUrl: string) {
    try {
      // For images that might fail CORS, just store the URL
      // The Web Share API will handle sharing the URL appropriately
      this.imageCache.set(imageUrl, null);
    } catch (error) {
      logger.warn("Failed to cache image:", error);
    }
  }

  /**
   * Opens image viewer dialog
   *
   * @public
   * Called by ActivityListItem component
   * @param imageUrl URL of image to display
   */
  async openImageViewer(imageUrl: string) {
    this.selectedImageData = this.imageCache.get(imageUrl) ?? null;
    this.selectedImage = imageUrl;
    this.isImageViewerOpen = true;
  }

  /**
   * Handles claim confirmation
   *
   * @public
   * Called by ActivityListItem component
   * @param record Record to confirm
   */
  async confirmClaim(record: GiveRecordWithContactInfo) {
    this.$notify(
      {
        group: "modal",
        type: "confirm",
        title: "Confirm",
        text: "Do you personally confirm that this is true?",
        onYes: async () => {
          const goodClaim = serverUtil.removeSchemaContext(
            serverUtil.removeVisibleToDids(
              serverUtil.addLastClaimOrHandleAsIdIfMissing(
                record.fullClaim,
                record.jwtId,
                record.handleId,
              ),
            ),
          );

          const confirmationClaim = {
            "@context": "https://schema.org",
            "@type": "AgreeAction",
            object: goodClaim,
          };

          const result = await serverUtil.createAndSubmitClaim(
            confirmationClaim,
            this.activeDid,
            this.apiServer,
            this.axios,
          );

          if (result.type === "success") {
            this.$notify(
              {
                group: "alert",
                type: "success",
                title: "Success",
                text: "Confirmation submitted.",
              },
              3000,
            );

            // Refresh the feed to show updated confirmation status
            await this.updateAllFeed();
          } else {
            logger.error("Error submitting confirmation:", result);
            this.$notify(
              {
                group: "alert",
                type: "danger",
                title: "Error",
                text: "There was a problem submitting the confirmation.",
              },
              5000,
            );
          }
        },
      },
      -1,
    );
  }

  private handleQRCodeClick() {
    if (Capacitor.isNativePlatform()) {
      this.$router.push({ name: "contact-qr-scan-full" });
    } else {
      this.$router.push({ name: "contact-qr" });
    }
  }
}
</script>