/** * @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 }}
      <span class="text-xs text-gray-500">{{ package.version }}</span>
    </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">
      <!-- 
      They should have an identifier, even if it's an auto-generated one that they'll never use.
      Identity creation is now handled by router navigation guard.
      -->
      <div class="mb-4">
        <div
          v-if="!isUserRegistered"
          id="noticeSomeoneMustRegisterYou"
          class="bg-amber-200 rounded-md overflow-hidden text-center px-4 py-3 mb-4"
        >
          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 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 advanced options
            </router-link>
          </div>
        </div>

        <div v-else id="sectionRecordSomethingGiven">
          <!-- Record Quick-Action -->
          <div class="mb-6">
            <div class="flex gap-2 items-center mb-2">
              <h2 class="text-xl font-bold">Record something given by:</h2>
              <button
                class="block ms-auto text-center text-white bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] p-2 rounded-full"
                @click="openGiftedPrompts()"
              >
                <font-awesome
                  icon="lightbulb"
                  class="block text-center w-[1em]"
                />
              </button>
            </div>

            <div class="grid grid-cols-2 gap-2">
              <button
                type="button"
                class="text-center text-base uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-3 py-2 rounded-lg"
                @click="openDialogPerson()"
              >
                <font-awesome icon="user" />
                Person
              </button>
              <button
                type="button"
                class="text-center text-base uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-3 py-2 rounded-lg"
                @click="openProjectDialog()"
              >
                <font-awesome icon="folder-open" />
                Project
              </button>
            </div>
          </div>
        </div>
      </div>
    </div>

    <GiftedDialog ref="customDialog" :show-projects="showProjectsDialog" />
    <GiftedPrompts ref="giftedPrompts" />
    <FeedFilters ref="feedFilters" />

    <!-- Results List -->
    <div class="mt-4 mb-4">
      <div class="flex gap-2 items-center mb-3">
        <h2 class="text-xl font-bold">Latest Activity</h2>
        <button
          v-if="resultsAreFiltered()"
          class="block ms-auto text-center text-white bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] p-2 rounded-full"
          @click="openFeedFilters()"
        >
          <font-awesome
            icon="filter"
            class="block text-center w-[1em] translate-y-[0.05em]"
          />
        </button>
        <button
          v-else
          class="block ms-auto text-center text-white bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] p-2 rounded-full"
          @click="openFeedFilters()"
        >
          <font-awesome
            icon="filter"
            class="block text-center w-[1em] translate-y-[0.05em]"
          />
        </button>
      </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,
} from "../constants/app";
import { Contact } from "../db/tables/contacts";
import { BoundingBox, checkIsAnyFeedFilterOn } from "../db/tables/settings";
import {
  contactForDid,
  containsNonHiddenDid,
  didInfoForContact,
  fetchEndorserRateLimits,
  getHeaders,
  getNewOffersToUser,
  getNewOffersToUserProjects,
  getPlanFromCache,
} from "../libs/endorserServer";
import {
  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 { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
import {
  NOTIFY_CONTACT_LOADING_ISSUE,
  NOTIFY_FEED_LOADING_ISSUE,
  NOTIFY_CONFIRMATION_ERROR,
} from "@/constants/notifications";
import * as Package from "../../package.json";

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,
  },
  mixins: [PlatformServiceMixin],
})
export default class HomeView extends Vue {
  $notify!: (notification: NotificationIface, timeout?: number) => void;
  $router!: Router;
  notify!: ReturnType<typeof createNotifyHelpers>;

  AppString = AppString;
  PASSKEYS_ENABLED = PASSKEYS_ENABLED;
  package = Package;

  activeDid = "";
  allContacts: Array<Contact> = [];
  allMyDids: Array<string> = [];
  apiServer = "";
  blockedContactDids: Array<string> = [];
  feedData: GiveRecordWithContactInfo[] = [];
  feedPreviousOldestId?: string;
  feedLastViewedClaimId?: string;
  givenName = "";
  isAnyFeedFilterOn = false;
  // isCreatingIdentifier removed - identity creation now handled by router guard
  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();
  showProjectsDialog = false;

  /**
   * Initializes notification helpers
   */
  created() {
    this.notify = createNotifyHelpers(this.$notify);
  }

  /**
   * 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();
      // Settings already loaded in initializeIdentity()
      await this.loadContacts();
      // Registration check already handled in initializeIdentity()
      await this.loadFeedData();
      await this.loadNewOffers();
      await this.checkOnboarding();
    } catch (err: unknown) {
      this.handleError(err);
    }
  }

  /**
   * Watch for changes in the current activeDid
   * Reload settings when user switches identities
   */
  async onActiveDidChanged(newDid: string | null, oldDid: string | null) {
    if (newDid !== oldDid) {
      // Re-initialize identity with new settings (loads settings internally)
      await this.initializeIdentity();
    }
  }

  /**
   * 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();
      } catch (error) {
        this.$logAndConsole(
          `[HomeView] Failed to retrieve DIDs: ${error}`,
          true,
        );
        throw new Error(
          "Failed to load existing identities. Please try restarting the app.",
        );
      }

      // Identity creation is now handled by router navigation guard
      // If we reach here, an identity should already exist
      if (this.allMyDids.length === 0) {
        this.$logAndConsole(
          `[HomeView] No identities found - this should not happen with router guard`,
          true,
        );
        throw new Error("No identity found. Please try refreshing the page.");
      }

      // Load settings with better error context using ultra-concise mixin
      let settings;
      try {
        settings = await this.$settings({
          apiServer: "",
          activeDid: "",
          isRegistered: false,
        });
      } catch (error) {
        this.$logAndConsole(
          `[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 || "";

      // **CRITICAL**: Ensure correct API server for platform
      await this.ensureCorrectApiServer();

      this.activeDid = settings.activeDid || "";

      // Load contacts with graceful fallback
      try {
        this.loadContacts();
      } catch (error) {
        this.$logAndConsole(
          `[HomeView] Failed to retrieve contacts: ${error}`,
          true,
        );
        this.allContacts = []; // Ensure we have a valid empty array
        this.blockedContactDids = [];
        this.notify.warning(
          NOTIFY_CONTACT_LOADING_ISSUE.message,
          TIMEOUTS.LONG,
        );
      }

      // 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) {
            // Ultra-concise settings update with automatic cache invalidation!
            await this.$saveMySettings({ isRegistered: true });
            this.isRegistered = true;
            // Force Vue to re-render the template
            await this.$nextTick();
          }
        } catch (error) {
          // Consolidate logging: Only log unexpected errors, not expected 400s
          const axiosError = error as any;
          if (axiosError?.response?.status !== 400) {
            this.$logAndConsole(
              `[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) => {
          this.$logAndConsole(
            `[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;
        }
      } catch (error) {
        this.$logAndConsole(
          `[HomeView] Failed to initialize feed/offers: ${error}`,
          true,
        );
        // Don't throw - we can continue with empty feed
        this.notify.warning(NOTIFY_FEED_LOADING_ISSUE.message, TIMEOUTS.LONG);
      }
    } catch (error) {
      this.handleError(error);
      throw error; // Re-throw to be caught by mounted()
    }
  }

  /**
   * Ensures API server is correctly set for the current platform
   * For Electron, always use production endpoint regardless of saved settings
   *
   * @internal
   * Called after loading settings to ensure correct API endpoint
   */
  private async ensureCorrectApiServer() {
    const { DEFAULT_ENDORSER_API_SERVER } = await import("../constants/app");

    if (process.env.VITE_PLATFORM === "electron") {
      // **CRITICAL FIX**: Always use production API server for Electron
      // This prevents the capacitor-electron:// protocol from being used for API calls
      this.apiServer = DEFAULT_ENDORSER_API_SERVER;
    } else if (!this.apiServer) {
      // **FIX**: Set default API server for web/development if not already set
      this.apiServer = DEFAULT_ENDORSER_API_SERVER;
    }
  }

  /**
   * Loads user settings from storage using ultra-concise mixin utilities
   * 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() {
    // Use the current activeDid (set in initializeIdentity) to get user-specific settings
    const settings = await this.$accountSettings(this.activeDid, {
      apiServer: "",
      activeDid: "",
      filterFeedByVisible: false,
      filterFeedByNearby: false,
      isRegistered: false,
    });

    this.apiServer = settings.apiServer || "";

    // **CRITICAL**: Ensure correct API server for platform
    await this.ensureCorrectApiServer();

    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 using ultra-concise mixin
   * Used for displaying contact info in feed and actions
   *
   * @internal
   * Called by mounted() and initializeIdentity()
   */
  private async loadContacts() {
    this.allContacts = await this.$contacts();
    this.blockedContactDids = this.allContacts
      .filter((c) => !c.iViewContent)
      .map((c) => c.did);
  }

  /**
   * 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) {
          // Ultra-concise settings update with automatic cache invalidation!
          await this.$saveMySettings({ isRegistered: true });
          this.isRegistered = true;
          // Force Vue to re-render the template
          await this.$nextTick();
        }
      } 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 using ultra-concise mixin utilities
   * Opens onboarding dialog if not completed
   *
   * @internal
   * Called by mounted()
   */
  private async checkOnboarding() {
    const settings = await this.$settings();
    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;

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

    this.notify.error(
      userMessage ||
        "There was an error loading your data. Please try refreshing the page.",
      TIMEOUTS.LONG,
    );
  }

  /**
   * 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 using ultra-concise mixin utilities
   * - Updates filter states
   * - Clears existing feed data
   * - Triggers new feed load
   *
   * @public
   * Called by FeedFilters component when filters change
   */
  async reloadFeedOnChange() {
    const settings = await this.$accountSettings(this.activeDid, {
      filterFeedByVisible: false,
      filterFeedByNearby: false,
    });
    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 data from endorser service with error handling
   * - Retrieves new gives from API
   * - Processes records through filters
   * - Updates last viewed claim ID
   * - Handles paging if needed
   *
   * @internal
   * @callGraph
   * Called by: loadFeedData(), manual refresh
   * Calls:
   * - retrieveGives()
   * - processFeedResults()
   * - updateFeedLastViewedId()
   * - handleFeedError()
   *
   * @chain
   * loadFeedData() -> updateAllFeed() -> retrieveGives()
   *
   * @requires
   * - this.apiServer
   * - 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;
        // gather any contacts that user has blocked from view
        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 & preferences
   *
   * @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.blockedContactDids.includes(record.issuerDid)) {
      return false;
    }

    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
    ) {
      // Ultra-concise default settings update with automatic cache invalidation!
      await this.$saveSettings({
        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.error(
      (e as FeedError)?.userMessage ||
        "There was an error retrieving feed data.",
      TIMEOUTS.MODAL,
    );
  }

  /**
   * Retrieves gift data from endorser API with error handling
   * - Fetches gives from API endpoint
   * - Handles authentication headers
   * - Processes API response with comprehensive error handling
   *
   * @public
   * @callGraph
   * Called by: updateAllFeed()
   * Calls:
   * - getHeaders()
   * - fetch()
   *
   * @chain
   * updateAllFeed() -> retrieveGives() -> getHeaders()
   *
   * @requires
   * - this.activeDid
   * - this.$notify
   *
   * @param endorserApiServer API server URL
   * @param beforeId Optional ID for pagination
   * @returns Promise resolving to API response data
   */
  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 responseText = await response.text();
    const results = JSON.parse(responseText);

    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 | "Unnamed", description?: string) {
    if (giver === "Unnamed") {
      // Special case: Pass undefined to trigger Step 1, but with "Unnamed" pre-selected
      (this.$refs.customDialog as GiftedDialog).open(
        undefined,
        {
          did: this.activeDid,
          name: "You",
        } as GiverReceiverInputInfo,
        undefined,
        "Given by Unnamed",
        description,
      );
      // Immediately select "Unnamed" and move to Step 2
      (this.$refs.customDialog as GiftedDialog).selectGiver();
    } else {
      (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.toast("FYI", message, TIMEOUTS.SHORT);
  }

  /**
   * 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.confirm(
      "Do you personally confirm that this is true?",
      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.success) {
          this.notify.confirmationSubmitted();

          // Refresh the feed to show updated confirmation status
          await this.updateAllFeed();
        } else {
          logger.error("Error submitting confirmation:", result);
          this.notify.error(NOTIFY_CONFIRMATION_ERROR.message, TIMEOUTS.LONG);
        }
      },
    );
  }

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

  openDialogPerson(
    giver?: GiverReceiverInputInfo | "Unnamed",
    description?: string,
  ) {
    this.showProjectsDialog = false;
    this.openDialog(giver, description);
  }

  openProjectDialog() {
    this.showProjectsDialog = true;
    (this.$refs.customDialog as GiftedDialog).open();
  }

  /**
   * Computed property for registration status
   *
   * @public
   * Used in template for registration-dependent UI elements
   */
  get isUserRegistered() {
    return this.isRegistered;
  }
}
</script>
