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

  <!-- CONTENT -->
  <section id="Content" class="p-2 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"
            <fa 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
            <fa
              icon="ellipsis-vertical"
              title="vertical ellipsis"
              class="fa-fw"
            />
            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">
          <fa 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
                @click="showNameThenIdDialog()"
                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"
              >
                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
                @click="openGiftedPrompts()"
                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"
              >
                <fa 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"
                  :iconSize="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()"
      >
        <fa icon="plus" class="fa-fw" />
      </button>
    </div>

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

      <div
        @click="goToActivityToUserPage()"
        class="border-t p-2 border-slate-300"
      >
        <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="border-t border-slate-300">
          <li
            class="border-b border-slate-300 py-2"
            v-for="record in feedData"
            :key="record.jwtId"
          >
            <div
              class="border-b border-slate-300 text-orange-400 pb-2 mb-2 font-bold text-sm"
              v-if="record.jwtId == feedLastViewedClaimId"
            >
              You've already seen all the following
            </div>

            <div class="grid grid-cols-12">
              <span class="pt-1 col-span-1 justify-self-start">
                <span>
                  <fa
                    icon="circle-user"
                    :class="
                      computeKnownPersonIconStyleClassNames(
                        record.giver.known || record.receiver.known,
                      )
                    "
                    @click="toastUser('This involves your contacts.')"
                  />
                  <fa
                    icon="gift"
                    class="pl-3 text-slate-500"
                    @click="toastUser('This is a gift.')"
                  />
                </span>
              </span>
              <span class="col-span-10 justify-self-stretch overflow-hidden">
                <!-- show giver and/or receiver profiles... which seemed like a good idea but actually adds clutter
                <span
                  v-if="
                    record.giver.profileImageUrl ||
                    record.receiver.profileImageUrl
                  "
                >
                  <EntityIcon
                    v-if="record.agentDid !== activeDid"
                    :icon-size="32"
                    :profile-image-url="record.giver.profileImageUrl"
                    class="inline-block align-middle border border-slate-300 rounded-md mr-1"
                  />
                  <fa
                    v-if="
                      record.agentDid !== activeDid &&
                      record.recipientDid !== activeDid &&
                      !record.fulfillsPlanHandleId
                    "
                    icon="ellipsis"
                    class="text-slate"
                  />
                  <EntityIcon
                    v-if="
                      record.recipientDid !== activeDid &&
                      !record.fulfillsPlanHandleId
                    "
                    :iconSize="32"
                    :profile-image-url="record.receiver.profileImageUrl"
                    class="inline-block align-middle border border-slate-300 rounded-md ml-1"
                  />
                </span>
                -->
                <span class="pl-2 block break-words">
                  {{ giveDescription(record) }}
                </span>
                <a @click="onClickLoadClaim(record.jwtId)">
                  <fa
                    icon="file-lines"
                    class="pl-2 text-slate-500 cursor-pointer"
                  />
                </a>
              </span>
              <span class="col-span-1 justify-self-end">
                <router-link
                  v-if="record.fulfillsPlanHandleId"
                  :to="
                    '/project/' +
                    encodeURIComponent(record.fulfillsPlanHandleId)
                  "
                >
                  <fa icon="hammer" class="text-blue-500" />
                </router-link>
                <router-link
                  v-if="record.providerPlanHandleId"
                  :to="
                    '/project/' +
                    encodeURIComponent(record.providerPlanHandleId)
                  "
                >
                  <fa icon="hammer" class="text-blue-500" />
                </router-link>
              </span>
            </div>
            <div v-if="record.image" class="w-full">
              <div
                class="cursor-pointer"
                @click="openImageViewer(record.image)"
              >
                <img
                  :src="record.image"
                  class="w-full aspect-[3/2] object-cover rounded-xl mt-2"
                  alt="shared content"
                  @load="cacheImageData($event, record.image)"
                />
              </div>
            </div>
          </li>
        </ul>
      </InfiniteScroll>
      <div v-if="isFeedLoading">
        <p class="text-slate-500 text-center italic mt-4 mb-4">
          <fa 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
    :image-url="selectedImage"
    :image-data="selectedImageData"
    v-model:is-open="isImageViewerOpen"
  />
</template>

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

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 {
  AppString,
  NotificationIface,
  PASSKEYS_ENABLED,
} 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 {
  contactForDid,
  containsNonHiddenDid,
  didInfoForContact,
  fetchEndorserRateLimits,
  getHeaders,
  getNewOffersToUser,
  getNewOffersToUserProjects,
  getPlanFromCache,
  GiveSummaryRecord,
} from "@/libs/endorserServer";
import {
  generateSaveAndActivateIdentity,
  retrieveAccountDids,
  GiverReceiverInputInfo,
  OnboardPage,
  registerSaveAndActivatePasskey,
} from "@/libs/util";

interface GiveRecordWithContactInfo extends GiveSummaryRecord {
  giver: {
    displayName: string;
    known: boolean;
    profileImageUrl?: string;
  };
  image?: string;
  providerPlanName?: string;
  recipientProjectName?: string;
  receiver: {
    displayName: string;
    known: boolean;
    profileImageUrl?: string;
  };
}

@Component({
  computed: {
    App() {
      return App;
    },
  },
  components: {
    EntityIcon,
    FeedFilters,
    GiftedDialog,
    GiftedPrompts,
    InfiniteScroll,
    OnboardingDialog,
    ChoiceButtonDialog,
    QuickNav,
    TopMessage,
    UserNameDialog,
    ImageViewer,
  },
})
export default class HomeView extends Vue {
  $notify!: (notification: NotificationIface, timeout?: number) => void;

  AppString = AppString;
  PASSKEYS_ENABLED = PASSKEYS_ENABLED;

  activeDid = "";
  allContacts: Array<Contact> = [];
  allMyDids: Array<string> = [];
  apiServer = "";
  feedData: GiveRecordWithContactInfo[] = [];
  feedPreviousOldestId?: string;
  feedLastViewedClaimId?: string;
  givenName = "";
  isAnyFeedFilterOn: boolean;
  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();

  async mounted() {
    try {
      try {
        this.allMyDids = await retrieveAccountDids();
        if (this.allMyDids.length === 0) {
          this.isCreatingIdentifier = true;
          const newDid = await generateSaveAndActivateIdentity();
          this.isCreatingIdentifier = false;
          this.allMyDids = [newDid];
        }
      } catch (error) {
        // continue because we want the feed to work, even anonymously
        logConsoleAndDb(
          "Error retrieving all account DIDs on home page:" + error,
          true,
        );
        // some other piece will display an error about personal info
      }

      const settings = await retrieveSettingsForActiveAccount();
      this.apiServer = settings.apiServer || "";
      this.activeDid = settings.activeDid || "";
      this.allContacts = await db.contacts.toArray();
      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);

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

      // someone may have have registered after sharing contact info, so recheck
      if (!this.isRegistered && this.activeDid) {
        try {
          const resp = await fetchEndorserRateLimits(
            this.apiServer,
            this.axios,
            this.activeDid,
          );
          if (resp.status === 200) {
            await updateAccountSettings(this.activeDid, {
              isRegistered: true,
            });
            this.isRegistered = true;
          }
        } catch (e) {
          // ignore the error... just keep us unregistered
        }
      }

      // this returns a Promise but we don't need to wait for it
      this.updateAllFeed();

      if (this.activeDid) {
        const offersToUserData = await getNewOffersToUser(
          this.axios,
          this.apiServer,
          this.activeDid,
          this.lastAckedOfferToUserJwtId,
        );
        this.numNewOffersToUser = offersToUserData.data.length;
        this.newOffersToUserHitLimit = offersToUserData.hitLimit;
      }

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

      // eslint-disable-next-line @typescript-eslint/no-explicit-any
    } catch (err: any) {
      logConsoleAndDb("Error retrieving settings or feed: " + err, true);
      this.$notify(
        {
          group: "alert",
          type: "danger",
          title: "Error",
          text:
            err.userMessage ||
            "There was an error retrieving your settings or the latest activity.",
        },
        5000,
      );
    }
  }

  async generatePasskeyIdentifier() {
    this.isCreatingIdentifier = true;
    const account = await registerSaveAndActivatePasskey(
      AppString.APP_NAME + (this.givenName ? " - " + this.givenName : ""),
    );
    this.activeDid = account.did;
    this.allMyDids = this.allMyDids.concat(this.activeDid);
    this.isCreatingIdentifier = false;
  }
  resultsAreFiltered() {
    return this.isFeedFilteredByVisible || this.isFeedFilteredByNearby;
  }

  notificationsSupported() {
    return "Notification" in window;
  }

  // only called when a setting was changed
  async reloadFeedOnChange() {
    const settings = await retrieveSettingsForActiveAccount();
    this.isFeedFilteredByVisible = !!settings.filterFeedByVisible;
    this.isFeedFilteredByNearby = !!settings.filterFeedByNearby;
    this.isAnyFeedFilterOn = checkIsAnyFeedFilterOn(settings);

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

  /**
   * Data loader used by infinite scroller
   * @param payload is the flag from the InfiniteScroll indicating if it should load
   **/
  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();
    }
  }

  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;
      }
    }
  }

  async updateAllFeed() {
    this.isFeedLoading = true;
    let endOfResults = true;
    await this.retrieveGives(this.apiServer, this.feedPreviousOldestId)
      .then(async (results) => {
        if (results.data.length > 0) {
          endOfResults = false;
          // include the descriptions of the giver and receiver
          for (const record: GiveSummaryRecord of results.data) {
            // 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 = (record.fullClaim as any).claim || record.fullClaim;
            // agent.did is for legacy data, before March 2023
            const giverDid =
              claim.agent?.identifier || (claim.agent as any)?.did; // eslint-disable-line @typescript-eslint/no-explicit-any
            // recipient.did is for legacy data, before March 2023
            const recipientDid =
              claim.recipient?.identifier || (claim.recipient as any)?.did; // eslint-disable-line @typescript-eslint/no-explicit-any

            // This has indeed proven problematic. See loadMoreGives
            // We should display it immediately and then get the plan later.
            const fulfillsPlan = await getPlanFromCache(
              record.fulfillsPlanHandleId,
              this.axios,
              this.apiServer,
              this.activeDid,
            );

            // check if the record should be filtered out
            let anyMatch = false;
            if (this.isFeedFilteredByVisible && containsNonHiddenDid(record)) {
              // has a visible DID so it's a keeper
              anyMatch = true;
            }
            if (!anyMatch && this.isFeedFilteredByNearby) {
              // check if the associated project has a location inside user's search box
              if (record.fulfillsPlanHandleId) {
                if (fulfillsPlan?.locLat && fulfillsPlan?.locLon) {
                  if (
                    this.latLongInAnySearchBox(
                      fulfillsPlan.locLat,
                      fulfillsPlan.locLon,
                    )
                  ) {
                    anyMatch = true;
                  }
                }
              }
            }
            if (this.isAnyFeedFilterOn && !anyMatch) {
              continue;
            }

            // checking for arrays due to legacy data
            const provider = Array.isArray(claim.provider)
              ? claim.provider[0]
              : claim.provider;
            const providedByPlan = await getPlanFromCache(
              provider?.identifier as string,
              this.axios,
              this.apiServer,
              this.activeDid,
            );

            const newRecord: GiveRecordWithContactInfo = {
              ...record,
              giver: didInfoForContact(
                giverDid,
                this.activeDid,
                contactForDid(giverDid, this.allContacts),
                this.allMyDids,
              ),
              image: claim.image,
              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,
              ),
            };
            this.feedData.push(newRecord);
          }
          this.feedPreviousOldestId =
            results.data[results.data.length - 1].jwtId;
          // The following update is only done on the first load.
          if (
            this.feedLastViewedClaimId == null ||
            this.feedLastViewedClaimId < results.data[0].jwtId
          ) {
            await db.open();
            await db.settings.update(MASTER_SETTINGS_KEY, {
              lastViewedClaimId: results.data[0].jwtId,
            });
          }
        }
      })
      .catch((e) => {
        console.error("Error with feed load:", e);
        this.$notify(
          {
            group: "alert",
            type: "danger",
            title: "Feed Error",
            text: e.userMessage || "There was an error retrieving feed data.",
          },
          -1,
        );
      });
    if (this.feedData.length === 0 && !endOfResults) {
      // repeat until there's at least some data
      await this.updateAllFeed();
    }
    this.isFeedLoading = false;
  }

  /**
   * Retrieve claims in reverse chronological order
   *
   * @param beforeId the earliest ID (of previous searches) to search earlier
   * @return 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);
    }
  }

  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 + ")";
    }
  }

  goToActivityToUserPage() {
    (this.$router as Router).push({ name: "new-activity" });
  }

  onClickLoadClaim(jwtId: string) {
    const route = {
      path: "/claim/" + encodeURIComponent(jwtId),
    };
    (this.$router as Router).push(route);
  }

  displayAmount(code: string, amt: number) {
    return "" + amt + " " + this.currencyShortWordForCode(code, amt === 1);
  }

  currencyShortWordForCode(unitCode: string, single: boolean) {
    return unitCode === "HUR" ? (single ? "hour" : "hours") : unitCode;
  }

  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,
    );
  }

  openGiftedPrompts() {
    (this.$refs.giftedPrompts as GiftedPrompts).open((giver, description) =>
      this.openDialog(giver as GiverReceiverInputInfo, description),
    );
  }

  openFeedFilters() {
    (this.$refs.feedFilters as FeedFilters).open(this.reloadFeedOnChange);
  }

  toastUser(message: string) {
    this.$notify(
      {
        group: "alert",
        type: "toast",
        title: "FYI",
        text: message,
      },
      2000,
    );
  }

  computeKnownPersonIconStyleClassNames(known: boolean) {
    return known ? "text-slate-500" : "text-slate-100";
  }

  showNameThenIdDialog() {
    if (!this.givenName) {
      (this.$refs.userNameDialog as UserNameDialog).open(() => {
        this.promptForShareMethod();
      });
    } else {
      this.promptForShareMethod();
    }
  }

  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 as Router).push({ name: "onboard-meeting-list" });
      },
      onOption2: () => {
        (this.$router as Router).push({ name: "contact-qr" });
      },
      onOption3: () => {
        (this.$router as Router).push({ name: "share-my-contact-info" });
      },
    });
  }

  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) {
      console.warn("Failed to cache image:", error);
    }
  }

  async openImageViewer(imageUrl: string) {
    this.selectedImageData = this.imageCache.get(imageUrl) ?? null;
    this.selectedImage = imageUrl;
    this.isImageViewerOpen = true;
  }
}
</script>