Browse Source

Merge branch 'master' into other-units

yml-fixes
anomalist 11 months ago
parent
commit
a0db6433a6
  1. 4
      README.md
  2. 42
      project.task.yaml
  3. 6
      src/libs/endorserServer.ts
  4. 3
      src/libs/util.ts
  5. 8
      src/main.ts
  6. 8
      src/router/index.ts
  7. 16
      src/views/AccountViewView.vue
  8. 1
      src/views/ContactsView.vue
  9. 65
      src/views/HelpNotificationsView.vue
  10. 15
      src/views/HelpView.vue
  11. 41
      src/views/NewEditProjectView.vue
  12. 84
      src/views/ProjectViewView.vue

4
README.md

@ -59,6 +59,10 @@ Under the "Your Identity" screen, click "Advanced", click "Switch Identity / No
For your own web-push tests, change the 'vapid' URL in App.vue, and install apps on the same domain. For your own web-push tests, change the 'vapid' URL in App.vue, and install apps on the same domain.
### Icons
To add an icon, add to main.ts and reference with `fa` element and `icon` attribute with the hyphenated name.
### Manual walk-through ### Manual walk-through
- Clear the browser cache for localhost for a new user. - Clear the browser cache for localhost for a new user.

42
project.task.yaml

@ -6,19 +6,40 @@ tasks:
- extract private_key_hex in py-push-server webpush.py - extract private_key_hex in py-push-server webpush.py
- lock down regenerate_vapid endpoint (so only we admins can do it on demand) - lock down regenerate_vapid endpoint (so only we admins can do it on demand)
- remove sleep in py-push-server app.py - remove sleep in py-push-server app.py
- revisit "maybe" and "never" buttons on accont screen
- see if we can detect OS-level notifications if turned off
- write troubleshooting docs for notifications
- .3 fix the Project-location-selection map display to not show on top of bottom icons (and any other UI tweaks on the map flow) assignee-group:ui - .3 fix the Project-location-selection map display to not show on top of bottom icons (and any other UI tweaks on the map flow) assignee-group:ui
- .5 Add infinite scroll to gifts on the home page - .5 Add infinite scroll to gifts on the home page
- Discuss whether the remaining tasks are worthwhile before MVP release. - .5 If notifications are not enabled, add message to front page with link/button to enable
- .1 Add units or different icon to the coins (to distinguish $, BTC, hours, etc) - show VC details... somehow:
- .5 make a VC details page, or link to endorser.ch (including confirmations) - .5 make a VC details page, or link to endorser.ch (including confirmations)
- 01 allow download of each VC (& confirmations, to show that they actually own their data) - 01 allow download of each VC (& confirmations, to show that they actually own their data)
- 04 allow user to download VCs, mine + ones I can see about me from others
- add VC confirmation?
- Release Minimum Viable Product :
- generate new webpush.db entry, webpush.py private_key_hex & subscription_info & vapid_claims email
- .5 deploy endorser.ch server above Dec 1 (to get plan searches by names as well as descriptions)
- 08 thorough testing for errors & edge cases
- 01 ensure ability to recover server remotely, and add redundant access
- Turn off stats-world or ensure it's usable (eg. cannot zoom out too far and lose world, cannot screenshot).
- Add disclaimers.
- Switch default server to the public server.
- Deploy to a server.
- Ensure public server has limits that work for group adoption.
- Test PWA features on Android and iOS.
blocks: ref:https://raw.githubusercontent.com/trentlarson/lives-of-gifts/master/project.yaml#kickstarter%20for%20time
- make identicons for contacts into more-memorable faces (and maybe change project identicons, too)
- allow some gives even if they aren't registered
- .5 Add start date to project
- .3 check that Android shows "back" buttons on screens without bottom tray - .3 check that Android shows "back" buttons on screens without bottom tray
- .1 Make give description text box into something that expands as they type? - .1 Make give description text box into something that expands as they type?
- 04 allow user to download claims, mine + ones I can see about me from others
- .5 customize favicon assignee-group:ui - .5 customize favicon assignee-group:ui
- .2 Show a warning if both giver and recipient are the same (but still allow?) - .2 Show a warning if both giver and recipient are the same (but still allow?)
- 01 Would it look better to shrink the buttons on many pages so they don't expand to the width of the screen? assignee-group:ui - 01 Would it look better to shrink the buttons on many pages so they don't expand to the width of the screen? assignee-group:ui
@ -26,8 +47,6 @@ tasks:
- .5 include the hash of the latest commit on help page next to version (maybe Trent's git-hash branch) - .5 include the hash of the latest commit on help page next to version (maybe Trent's git-hash branch)
- .5 remove references to localStorage for projectId (now that it's pulling from the path) - .5 remove references to localStorage for projectId (now that it's pulling from the path)
- bug (that is hard to reproduce) - on the second 'give' recorded on prod it showed me as the agent - bug (that is hard to reproduce) - on the second 'give' recorded on prod it showed me as the agent
- make identicons for contacts into more-memorable faces (and maybe change project identicons, too)
- allow some gives even if they aren't registered
- switch some checks for activeDid to check for isRegistered - switch some checks for activeDid to check for isRegistered
- .2 in SeedBackupView, don't load the mnemonic and keep it in memory; only load it when they click "show" - .2 in SeedBackupView, don't load the mnemonic and keep it in memory; only load it when they click "show"
- .5 fix cert generation on server (since it didn't happen automatically for Nov 30) - .5 fix cert generation on server (since it didn't happen automatically for Nov 30)
@ -43,19 +62,6 @@ tasks:
- maybe - allow type annotations in World.js & landmarks.js (since we get this error - "Types are not supported by current JavaScript version") - maybe - allow type annotations in World.js & landmarks.js (since we get this error - "Types are not supported by current JavaScript version")
- 08 convert to cleaner implementation (maybe Drie -- https://github.com/janvorisek/drie) - 08 convert to cleaner implementation (maybe Drie -- https://github.com/janvorisek/drie)
- Release Minimum Viable Product :
- generate new webpush.db entries, data/webpush.db private_key_hex & subscription_info & vapid_claims email
- .5 deploy endorser.ch server above Dec 1 (to get plan searches by names as well as descriptions)
- 08 thorough testing for errors & edge cases
- 01 ensure ability to recover server remotely, and add redundant access
- Turn off stats-world or ensure it's usable (eg. cannot zoom out too far and lose world, cannot screenshot).
- Add disclaimers.
- Switch default server to the public server.
- Deploy to a server.
- Ensure public server has limits that work for group adoption.
- Test PWA features on Android and iOS.
blocks: ref:https://raw.githubusercontent.com/trentlarson/lives-of-gifts/master/project.yaml#kickstarter%20for%20time
- .5 show seed phrase in a QR code for transfer to another device - .5 show seed phrase in a QR code for transfer to another device
- .5 on DiscoverView, switch to a filter UI (eg. just from friend - .5 on DiscoverView, switch to a filter UI (eg. just from friend
- .5 don't show "Offer" on project screen if they aren't registered - .5 don't show "Offer" on project screen if they aren't registered

6
src/libs/endorserServer.ts

@ -69,6 +69,8 @@ export interface OfferServerRecord {
validThrough: string; validThrough: string;
} }
// Note that previous VCs may have additional fields.
// https://endorser.ch/doc/html/transactions.html#id4
export interface GiveVerifiableCredential { export interface GiveVerifiableCredential {
"@context"?: string; // optional when embedded, eg. in an Agree "@context"?: string; // optional when embedded, eg. in an Agree
"@type": "GiveAction"; "@type": "GiveAction";
@ -80,6 +82,8 @@ export interface GiveVerifiableCredential {
recipient?: { identifier: string }; recipient?: { identifier: string };
} }
// Note that previous VCs may have additional fields.
// https://endorser.ch/doc/html/transactions.html#id8
export interface OfferVerifiableCredential { export interface OfferVerifiableCredential {
"@context"?: string; // optional when embedded, eg. in an Agree "@context"?: string; // optional when embedded, eg. in an Agree
"@type": "Offer"; "@type": "Offer";
@ -93,6 +97,8 @@ export interface OfferVerifiableCredential {
validThrough?: string; validThrough?: string;
} }
// Note that previous VCs may have additional fields.
// https://endorser.ch/doc/html/transactions.html#id7
export interface PlanVerifiableCredential { export interface PlanVerifiableCredential {
"@context": "https://schema.org"; "@context": "https://schema.org";
"@type": "PlanAction"; "@type": "PlanAction";

3
src/libs/util.ts

@ -0,0 +1,3 @@
export const isGlobalUri = (uri: string) => {
return uri && uri.match(new RegExp(/^[A-Za-z][A-Za-z0-9+.-]+:/));
};

8
src/main.ts

@ -14,6 +14,7 @@ import {
faArrowLeft, faArrowLeft,
faArrowRight, faArrowRight,
faBan, faBan,
faBitcoinSign,
faBurst, faBurst,
faCalendar, faCalendar,
faChevronLeft, faChevronLeft,
@ -27,6 +28,7 @@ import {
faCoins, faCoins,
faComment, faComment,
faCopy, faCopy,
faDollar,
faEllipsisVertical, faEllipsisVertical,
faEye, faEye,
faEyeSlash, faEyeSlash,
@ -34,6 +36,7 @@ import {
faFloppyDisk, faFloppyDisk,
faFolderOpen, faFolderOpen,
faGift, faGift,
faGlobe,
faHand, faHand,
faHouseChimney, faHouseChimney,
faLocationDot, faLocationDot,
@ -44,6 +47,7 @@ import {
faPersonCircleCheck, faPersonCircleCheck,
faPersonCircleQuestion, faPersonCircleQuestion,
faPlus, faPlus,
faQuestion,
faQrcode, faQrcode,
faRotate, faRotate,
faShareNodes, faShareNodes,
@ -61,6 +65,7 @@ library.add(
faArrowLeft, faArrowLeft,
faArrowRight, faArrowRight,
faBan, faBan,
faBitcoinSign,
faBurst, faBurst,
faCalendar, faCalendar,
faChevronLeft, faChevronLeft,
@ -74,6 +79,7 @@ library.add(
faCoins, faCoins,
faComment, faComment,
faCopy, faCopy,
faDollar,
faEllipsisVertical, faEllipsisVertical,
faEye, faEye,
faEyeSlash, faEyeSlash,
@ -81,6 +87,7 @@ library.add(
faFloppyDisk, faFloppyDisk,
faFolderOpen, faFolderOpen,
faGift, faGift,
faGlobe,
faHand, faHand,
faHouseChimney, faHouseChimney,
faLocationDot, faLocationDot,
@ -92,6 +99,7 @@ library.add(
faPersonCircleQuestion, faPersonCircleQuestion,
faPlus, faPlus,
faQrcode, faQrcode,
faQuestion,
faRotate, faRotate,
faShareNodes, faShareNodes,
faSpinner, faSpinner,

8
src/router/index.ts

@ -91,6 +91,14 @@ const routes: Array<RouteRecordRaw> = [
component: () => component: () =>
import(/* webpackChunkName: "help" */ "../views/HelpView.vue"), import(/* webpackChunkName: "help" */ "../views/HelpView.vue"),
}, },
{
path: "/help-notifications",
name: "help-notifications",
component: () =>
import(
/* webpackChunkName: "help-notifications" */ "../views/HelpNotificationsView.vue"
),
},
{ {
path: "/identity-switcher", path: "/identity-switcher",
name: "identity-switcher", name: "identity-switcher",

16
src/views/AccountViewView.vue

@ -288,6 +288,14 @@
</div> </div>
</label> </label>
<div class="flex py-2">
<button class="text-blue-500">
<router-link :to="{ name: 'statistics' }" class="block text-center">
See Global Animated History of Giving
</router-link>
</button>
</div>
<div class="flex py-2"> <div class="flex py-2">
<button class="text-blue-500"> <button class="text-blue-500">
<!-- id used by puppeteer test script --> <!-- id used by puppeteer test script -->
@ -301,14 +309,6 @@
</button> </button>
</div> </div>
<div class="flex py-2">
<button class="text-blue-500">
<router-link :to="{ name: 'statistics' }" class="block text-center">
See Achievements & Statistics
</router-link>
</button>
</div>
<div class="flex py-4"> <div class="flex py-4">
<h2 class="text-slate-500 text-sm font-bold mb-2">Claim Server</h2> <h2 class="text-slate-500 text-sm font-bold mb-2">Claim Server</h2>
<input <input

1
src/views/ContactsView.vue

@ -957,6 +957,7 @@ export default class ContactsView extends Vue {
} }
} }
// similar function is in endorserServer.ts
private async createAndSubmitGive( private async createAndSubmitGive(
identity: IIdentifier, identity: IIdentifier,
fromDid: string, fromDid: string,

65
src/views/HelpNotificationsView.vue

@ -0,0 +1,65 @@
<template>
<QuickNav />
<!-- CONTENT -->
<section id="Content" class="p-6 pb-24">
<!-- Breadcrumb -->
<div class="mb-8">
<!-- Back -->
<div class="text-lg text-center font-light relative px-7">
<h1
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
@click="$router.back()"
>
<fa icon="chevron-left" class="fa-fw"></fa>
</h1>
</div>
<!-- Heading -->
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
Notification Help
</h1>
</div>
<div>
<p>Here are things to try to get notifications working.</p>
<h2 class="text-xl font-semibold">Test</h2>
<p>Somehow call the service-worker self.showNotification</p>
<h2 class="text-xl font-semibold">Check OS-level permissions</h2>
<p>
Walk-throughs & screenshots, maybe for all combinations of OS &
browsers.
</p>
<h2 class="text-xl font-semibold">Check browser-level permissions</h2>
<p>Walk-throughs & screenshots for browser settings</p>
<h2 class="text-xl font-semibold">Explain full reset to start again</h2>
<p>
Walk-throughs for clearing everything & subscribing anew to get a
message
</p>
<h2 class="text-xl font-semibold">Auto-detection</h2>
<p>Show results of auto-detection whether they're turned on</p>
</div>
</section>
</template>
<script lang="ts">
import { Component, Vue } from "vue-facing-decorator";
import QuickNav from "@/components/QuickNav.vue";
interface Notification {
group: string;
type: string;
title: string;
text: string;
}
@Component({ components: { QuickNav } })
export default class HelpNotificationsView extends Vue {
$notify!: (notification: Notification, timeout?: number) => void;
}
</script>

15
src/views/HelpView.vue

@ -181,6 +181,21 @@
different page. different page.
</p> </p>
<h2 class="text-xl font-semibold">
How do I access even more functionality?
</h2>
<p>
There is an "Advanced" section at the bottom of the Account
<fa icon="circle-user" /> page.
</p>
<p>
There is a even more functionality in a mobile app (and more
documentation) at
<a href="https://endorser.ch" class="text-blue-500">
EndorserSearch.com
</a>
</p>
<h2 class="text-xl font-semibold">What is your privacy policy?</h2> <h2 class="text-xl font-semibold">What is your privacy policy?</h2>
<p> <p>
See See

41
src/views/NewEditProjectView.vue

@ -26,20 +26,26 @@
type="text" type="text"
placeholder="Idea Name" placeholder="Idea Name"
class="block w-full rounded border border-slate-400 mb-4 px-3 py-2" class="block w-full rounded border border-slate-400 mb-4 px-3 py-2"
v-model="projectName" v-model="fullClaim.name"
/> />
<textarea <textarea
placeholder="Description" placeholder="Description"
class="block w-full rounded border border-slate-400 mb-4 px-3 py-2" class="block w-full rounded border border-slate-400 mb-4 px-3 py-2"
rows="5" rows="5"
v-model="description" v-model="fullClaim.description"
maxlength="5000" maxlength="5000"
></textarea> ></textarea>
<div class="text-xs text-slate-500 italic -mt-3 mb-4"> <div class="text-xs text-slate-500 italic -mt-3 mb-4">
{{ description.length }}/5000 max. characters {{ fullClaim.description.length }}/5000 max. characters
</div> </div>
<input
v-model="fullClaim.url"
placeholder="Website"
class="block w-full rounded border border-slate-400 mb-4 px-3 py-2"
/>
<div class="flex items-center mb-4"> <div class="flex items-center mb-4">
<input <input
type="checkbox" type="checkbox"
@ -72,7 +78,7 @@
name="OpenStreetMap" name="OpenStreetMap"
/> />
<l-marker <l-marker
v-if="latitude || longitude" v-if="latitude && longitude"
:lat-lng="[latitude, longitude]" :lat-lng="[latitude, longitude]"
@click="maybeEraseLatLong()" @click="maybeEraseLatLong()"
/> />
@ -136,13 +142,17 @@ export default class NewEditProjectView extends Vue {
activeDid = ""; activeDid = "";
apiServer = ""; apiServer = "";
description = "";
errorMessage = ""; errorMessage = "";
fullClaim: PlanVerifiableCredential = {
"@context": "https://schema.org",
"@type": "PlanAction",
name: "",
description: "",
}; // this default is only to avoid errors before plan is loaded
includeLocation = false; includeLocation = false;
latitude = 0; latitude = 0;
longitude = 0; longitude = 0;
numAccounts = 0; numAccounts = 0;
projectName = "";
zoom = 2; zoom = 2;
async beforeCreate() { async beforeCreate() {
@ -214,9 +224,12 @@ export default class NewEditProjectView extends Vue {
try { try {
const resp = await this.axios.get(url, { headers }); const resp = await this.axios.get(url, { headers });
if (resp.status === 200) { if (resp.status === 200) {
const claim = resp.data.claim; this.fullClaim = resp.data.claim;
this.projectName = claim.name; if (this.fullClaim?.location) {
this.description = claim.description; this.includeLocation = true;
this.latitude = this.fullClaim.location.geo.latitude;
this.longitude = this.fullClaim.location.geo.longitude;
}
} }
} catch (error) { } catch (error) {
console.error("Got error retrieving that project", error); console.error("Got error retrieving that project", error);
@ -225,13 +238,7 @@ export default class NewEditProjectView extends Vue {
private async SaveProject(identity: IIdentifier) { private async SaveProject(identity: IIdentifier) {
// Make a claim // Make a claim
const vcClaim: PlanVerifiableCredential = { const vcClaim: PlanVerifiableCredential = this.fullClaim;
"@context": "https://schema.org",
"@type": "PlanAction",
name: this.projectName,
description: this.description,
identifier: this.projectId || undefined,
};
if (this.projectId) { if (this.projectId) {
vcClaim.identifier = this.projectId; vcClaim.identifier = this.projectId;
} }
@ -314,8 +321,8 @@ export default class NewEditProjectView extends Vue {
error?: { message?: string }; error?: { message?: string };
}>; }>;
if (serverError) { if (serverError) {
console.log("Got error from server", serverError);
if (Object.prototype.hasOwnProperty.call(serverError, "message")) { if (Object.prototype.hasOwnProperty.call(serverError, "message")) {
console.log(serverError);
userMessage = serverError.response?.data?.error?.message || ""; // This is info for the user. userMessage = serverError.response?.data?.error?.message || ""; // This is info for the user.
this.$notify( this.$notify(
{ {

84
src/views/ProjectViewView.vue

@ -35,7 +35,7 @@
<fa icon="user" class="fa-fw text-slate-400"></fa> <fa icon="user" class="fa-fw text-slate-400"></fa>
{{ issuer }} {{ issuer }}
</div> </div>
<div> <div v-if="timeSince">
<fa icon="calendar" class="fa-fw text-slate-400"></fa> <fa icon="calendar" class="fa-fw text-slate-400"></fa>
{{ timeSince }} {{ timeSince }}
</div> </div>
@ -45,8 +45,13 @@
:href="getOpenStreetMapUrl()" :href="getOpenStreetMapUrl()"
target="_blank" target="_blank"
class="underline" class="underline"
> >Map View
Map View </a>
</div>
<div v-if="url">
<fa icon="globe" class="fa-fw text-slate-400"></fa>
<a :href="addScheme(url)" target="_blank" class="underline"
>{{ domainForWebsite(this.url) }}
</a> </a>
</div> </div>
</div> </div>
@ -56,8 +61,11 @@
<div class="text-sm text-slate-500"> <div class="text-sm text-slate-500">
<div v-if="!expanded"> <div v-if="!expanded">
{{ truncatedDesc }} {{ truncatedDesc }}
<a v-if="description.length >= truncateLength" @click="expandText" <a
>Read More</a v-if="description.length >= truncateLength"
@click="expandText"
class="uppercase text-xs font-semibold text-slate-700"
>... Read More</a
> >
</div> </div>
<div v-else> <div v-else>
@ -65,7 +73,7 @@
<a <a
@click="collapseText" @click="collapseText"
class="uppercase text-xs font-semibold text-slate-700" class="uppercase text-xs font-semibold text-slate-700"
>Read Less</a >- Read Less</a
> >
</div> </div>
</div> </div>
@ -167,8 +175,10 @@
{{ didInfo(offer.agentDid, activeDid, allMyDids, allContacts) }} {{ didInfo(offer.agentDid, activeDid, allMyDids, allContacts) }}
</span> </span>
<span v-if="offer.amount"> <span v-if="offer.amount">
<fa icon="coins" class="fa-fw text-slate-400"></fa> <fa
{{ offer.amount }} :icon="iconForUnitCode(offer.unit)"
class="fa-fw text-slate-400"
/>{{ offer.amount }}
</span> </span>
</div> </div>
<div v-if="offer.objectDescription" class="text-slate-500"> <div v-if="offer.objectDescription" class="text-slate-500">
@ -195,9 +205,11 @@
><fa icon="user" class="fa-fw text-slate-400"></fa> ><fa icon="user" class="fa-fw text-slate-400"></fa>
{{ didInfo(give.agentDid, activeDid, allMyDids, allContacts) }} {{ didInfo(give.agentDid, activeDid, allMyDids, allContacts) }}
</span> </span>
<span v-if="give.amount" <span v-if="give.amount">
><fa icon="coins" class="fa-fw text-slate-400"></fa> <fa
{{ give.amount }} :icon="iconForUnitCode(give.unit)"
class="fa-fw text-slate-400"
/>{{ give.amount }}
</span> </span>
</div> </div>
<div v-if="give.description" class="text-slate-500"> <div v-if="give.description" class="text-slate-500">
@ -265,6 +277,7 @@ import { accountsDB, db } from "@/db/index";
import { Contact } from "@/db/tables/contacts"; import { Contact } from "@/db/tables/contacts";
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings"; import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
import { accessToken } from "@/libs/crypto"; import { accessToken } from "@/libs/crypto";
import { isGlobalUri } from "@/libs/util";
import { import {
didInfo, didInfo,
GiverInputInfo, GiverInputInfo,
@ -307,6 +320,7 @@ export default class ProjectViewView extends Vue {
timeSince = ""; timeSince = "";
truncatedDesc = ""; truncatedDesc = "";
truncateLength = 40; truncateLength = 40;
url = "";
async created() { async created() {
await db.open(); await db.open();
@ -408,6 +422,7 @@ export default class ProjectViewView extends Vue {
this.truncatedDesc = this.description.slice(0, this.truncateLength); this.truncatedDesc = this.description.slice(0, this.truncateLength);
this.latitude = resp.data.claim?.location?.geo?.latitude || 0; this.latitude = resp.data.claim?.location?.geo?.latitude || 0;
this.longitude = resp.data.claim?.location?.geo?.longitude || 0; this.longitude = resp.data.claim?.location?.geo?.longitude || 0;
this.url = resp.data.claim?.url || "";
} else if (resp.status === 404) { } else if (resp.status === 404) {
// actually, axios throws an error so we never get here // actually, axios throws an error so we never get here
this.$notify( this.$notify(
@ -625,5 +640,52 @@ export default class ProjectViewView extends Vue {
openOfferDialog() { openOfferDialog() {
(this.$refs.customOfferDialog as OfferDialog).open(); (this.$refs.customOfferDialog as OfferDialog).open();
} }
UNIT_CODES: Record<string, Record<string, string>> = {
BTC: {
name: "Bitcoin",
faIcon: "bitcoin-sign",
},
HUR: {
name: "hours",
faIcon: "clock",
},
USD: {
name: "US Dollars",
faIcon: "dollar",
},
};
iconForUnitCode(unitCode: string) {
return this.UNIT_CODES[unitCode]?.faIcon || "question";
}
// return an HTTPS URL if it's not a global URL
addScheme(url: string) {
if (!isGlobalUri(url)) {
return "https://" + url;
}
return url;
}
// return just the domain for display, if possible
domainForWebsite(url: string) {
try {
const hostname = new URL(url).hostname;
if (!hostname) {
// happens for non-http URLs
return url;
} else if (url.endsWith(hostname)) {
// it's just the domain
return hostname;
} else {
// there's more, but don't bother displaying the whole thing
return hostname + "...";
}
} catch (error: unknown) {
// must not be a valid URL
return url;
}
}
} }
</script> </script>

Loading…
Cancel
Save