Compare commits

...

13 Commits

Author SHA1 Message Date
130d7ecb01 Merge branch 'feat/image-feed-view-improvements' into fix/adding-nostr-tools-dependency 2025-02-14 15:38:19 -05:00
6fc416d759 chore: cleanup in prep for pull request 2025-02-14 13:39:07 -07:00
aa0156cdf2 docs(feed): documenting improvements to the image view in the home feed 2025-02-14 13:32:36 -07:00
e86cf3c9f2 deps: fixing issues around nostr-tools deps being removed that were necessary 2025-02-14 13:24:29 -07:00
f4c7805266 feat(feed): improving the image viewer, to be more conventional, and also allowing the viewer to download the image on mobile with ... control 2025-02-14 13:06:36 -07:00
0511bbc17b feat(feed): adding image viewer for expanding images found in the feed instead of displaying in a new tab and taking the viewer away from the application 2025-02-14 11:52:20 -07:00
be1146c7df fix(feed): long words or urls displayed in feed break the container layout 2025-02-14 11:46:17 -07:00
77ebd32956 deps: updating the nostr-tools version from 2.7.2 to 2.10.4 to fix a bug I was seeing locally 2025-02-14 09:59:47 -07:00
781afd8954 feat(feed): better image formatting, to take up the width of the feed container 2025-02-14 09:58:55 -07:00
53a06be734 deps: updated nostr-tools and import 2025-02-14 06:39:46 -07:00
cc14b9e0ce bump version to 0.3.57 2025-02-11 09:13:02 -07:00
9a6b2fcf0d for meeting invitees, create their ID and allow them to set a name 2025-02-11 08:15:01 -07:00
2b78307a51 bump version & add "-beta" 2025-02-07 15:23:12 -07:00
11 changed files with 391 additions and 64 deletions

View File

@@ -6,6 +6,22 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [0.4.0] - 2025.02.14
### Changed
- Images in the home feed now take up the full width of the card.
- Clicking the image previously, would open the image in a new tab. Now, clicking the image opens the image in a lightbox view.
### Added
- Clicking an image also now displays an in-app lightbox view of the image.
- The lightbox view includes a download button for the image in mobile view.
## [0.3.57] - 2025.02.11
### Added
- Automatic user creation in onboarding meetings
## [0.3.55] - 2025.02.07
### Added
- End time for projects

View File

@@ -50,7 +50,7 @@ Look below for the "test-all" instructions.
* Put the commit hash in the changelog (which will help you remember to bump the version later).
* Tag with the new version, [online](https://gitea.anomalistdesign.com/trent_larson/crowd-funder-for-time-pwa/releases) or `git tag 0.3.36` && `git push origin 0.3.36`.
* Tag with the new version, [online](https://gitea.anomalistdesign.com/trent_larson/crowd-funder-for-time-pwa/releases) or `git tag 0.3.55 && git push origin 0.3.55`.
* For test, build the app (because test server is not yet set up to build):
@@ -70,7 +70,7 @@ TIME_SAFARI_APP_TITLE="TimeSafari_Test" VITE_APP_SERVER=https://test.timesafari.
* `pkgx +npm sh`
* `cd crowd-funder-for-time-pwa && git checkout master && git pull && git checkout 0.3.36 && npm install && npm run build && cd -`
* `cd crowd-funder-for-time-pwa && git checkout master && git pull && git checkout 0.3.55 && npm install && npm run build && cd -`
(The plain `npm run build` uses the .env.production file.)

15
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "TimeSafari",
"version": "0.3.55",
"version": "0.3.57",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "TimeSafari",
"version": "0.3.55",
"version": "0.3.57",
"dependencies": {
"@capacitor/android": "^6.1.2",
"@capacitor/cli": "^6.1.2",
@@ -54,7 +54,7 @@
"lru-cache": "^10.2.0",
"luxon": "^3.4.4",
"merkletreejs": "^0.3.11",
"nostr-tools": "^2.7.2",
"nostr-tools": "^2.10.4",
"notiwind": "^2.0.2",
"papaparse": "^5.4.1",
"pina": "^0.20.2204228",
@@ -18905,9 +18905,10 @@
}
},
"node_modules/nostr-tools": {
"version": "2.7.2",
"resolved": "https://registry.npmjs.org/nostr-tools/-/nostr-tools-2.7.2.tgz",
"integrity": "sha512-Bq3Ug0SZFtgtL1+0wCnAe8AJtI7yx/00/a2nUug9SkhfOwlKS92Tef12iCK9FdwXw+oFZWMtRnSwcLayQso+xA==",
"version": "2.10.4",
"resolved": "https://registry.npmjs.org/nostr-tools/-/nostr-tools-2.10.4.tgz",
"integrity": "sha512-biU7sk+jxHgVASfobg2T5ttxOGGSt69wEVBC51sHHOEaKAAdzHBLV/I2l9Rf61UzClhliZwNouYhqIso4a3HYg==",
"license": "Unlicense",
"dependencies": {
"@noble/ciphers": "^0.5.1",
"@noble/curves": "1.2.0",
@@ -18917,7 +18918,7 @@
"@scure/bip39": "1.2.1"
},
"optionalDependencies": {
"nostr-wasm": "v0.1.0"
"nostr-wasm": "0.1.0"
},
"peerDependencies": {
"typescript": ">=5.0.0"

View File

@@ -1,6 +1,6 @@
{
"name": "TimeSafari",
"version": "0.3.55",
"version": "0.3.57",
"scripts": {
"dev": "vite",
"serve": "vite preview",
@@ -58,7 +58,7 @@
"lru-cache": "^10.2.0",
"luxon": "^3.4.4",
"merkletreejs": "^0.3.11",
"nostr-tools": "^2.7.2",
"nostr-tools": "^2.10.4",
"notiwind": "^2.0.2",
"papaparse": "^5.4.1",
"pina": "^0.20.2204228",

View File

@@ -0,0 +1,97 @@
<template>
<Teleport to="body">
<Transition name="fade">
<div
v-if="isOpen"
class="fixed inset-0 z-50 flex flex-col bg-black/90"
>
<!-- Header bar - fixed height to prevent overlap -->
<div class="h-16 flex justify-between items-center px-4 bg-black">
<button
class="text-white text-2xl p-2 rounded-full hover:bg-white/10"
@click="close"
>
<fa icon="xmark" />
</button>
<!-- Mobile share button -->
<button
v-if="isMobile"
class="text-white text-xl p-2 rounded-full hover:bg-white/10"
@click="handleShare"
>
<fa icon="ellipsis" />
</button>
</div>
<!-- Image container - fill remaining space -->
<div class="flex-1 flex items-center justify-center p-2">
<div class="w-full h-full flex items-center justify-center">
<img
:src="imageUrl"
class="max-h-[calc(100vh-5rem)] w-full h-full object-contain"
@click.stop
alt="expanded shared content"
/>
</div>
</div>
</div>
</Transition>
</Teleport>
</template>
<script lang="ts">
import { Component, Vue, Prop } from "vue-facing-decorator";
import { UAParser } from "ua-parser-js";
@Component({ emits: ["update:isOpen"] })
export default class ImageViewer extends Vue {
@Prop() imageUrl!: string;
@Prop() imageData!: Blob | null;
@Prop() isOpen!: boolean;
userAgent = new UAParser();
get isMobile() {
const os = this.userAgent.getOS().name;
return os === "iOS" || os === "Android";
}
close() {
this.$emit("update:isOpen", false);
}
async handleShare() {
const os = this.userAgent.getOS().name;
try {
if (os === "iOS" || os === "Android") {
if (navigator.share) {
// Always share the URL since it's more reliable across platforms
await navigator.share({
url: this.imageUrl
});
} else {
// Fallback for browsers without share API
window.open(this.imageUrl, "_blank");
}
}
} catch (error) {
console.warn("Share failed, opening in new tab:", error);
window.open(this.imageUrl, "_blank");
}
}
}
</script>
<style scoped>
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.2s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
</style>

View File

@@ -1,40 +1,41 @@
<template>
<div class="space-y-4">
<!-- Loading State -->
<div v-if="isLoading" class="flex justify-center items-center py-8">
<div
v-if="isLoading"
class="mt-16 text-center text-4xl bg-slate-400 text-white w-14 py-2.5 rounded-full mx-auto"
>
<fa icon="spinner" class="fa-spin-pulse" />
</div>
<!-- Members List -->
<p
v-if="decryptedMembers.length < members.length"
class="text-center text-red-600 py-4"
>
{{
decryptFailureMessage ||
"Your password failed. Please go back and try again."
}}
</p>
<div v-else>
<div class="text-center text-red-600 py-4">
{{ decryptionErrorMessage() }}
</div>
<div v-else class="space-y-4">
<div v-if="missingMyself" class="py-4 text-red-600">
You are not admitted. The organizer will admit you.
You are not currently admitted by the organizer.
</div>
<div v-if="!firstName" class="py-4 text-red-600">
Your name is not set, so others may not recognize you. Reload this page
to set it.
</div>
<div>
<span
v-if="showOrganizerTools && isOrganizer"
v-if="membersToShow().length > 0 && showOrganizerTools && isOrganizer"
class="inline-flex items-center flex-wrap"
>
<span class="inline-flex items-center">
Use
&bull; Click
<span
class="mx-2 min-w-[24px] min-h-[24px] w-6 h-6 flex items-center justify-center rounded-full bg-blue-100 text-blue-600"
>
<fa icon="plus" class="text-sm" />
</span>
and
/
<span
class="mx-2 min-w-[24px] min-h-[24px] w-6 h-6 flex items-center justify-center rounded-full bg-blue-100 text-blue-600"
>
@@ -45,8 +46,11 @@
</span>
</div>
<div>
<span class="inline-flex items-center">
Use
<span
v-if="membersToShow().length > 0"
class="inline-flex items-center"
>
&bull; Click
<span
class="mx-2 w-8 h-8 flex items-center justify-center rounded-full bg-green-100 text-green-600"
>
@@ -56,7 +60,8 @@
</span>
</div>
<div v-if="members.length > 0" class="flex justify-center">
<div class="flex justify-center">
<!-- always have at least one refresh button even without members in case the organizer changes the password -->
<button
@click="fetchMembers"
class="w-8 h-8 flex items-center justify-center rounded-full bg-blue-100 text-blue-600 hover:bg-blue-200 hover:text-blue-800 transition-colors"
@@ -68,7 +73,7 @@
<div
v-for="member in membersToShow()"
:key="member.member.memberId"
class="p-4 bg-gray-50 rounded-lg"
class="mt-2 p-4 bg-gray-50 rounded-lg"
>
<div class="flex items-center justify-between">
<div class="flex items-center">
@@ -131,7 +136,7 @@
{{ member.did }}
</p>
</div>
<div v-if="members.length > 0" class="flex justify-center mt-4">
<div v-if="membersToShow().length > 0" class="flex justify-center mt-4">
<button
@click="fetchMembers"
class="w-8 h-8 flex items-center justify-center rounded-full bg-blue-100 text-blue-600 hover:bg-blue-200 hover:text-blue-800 transition-colors"
@@ -187,16 +192,15 @@ export default class MembersList extends Vue {
libsUtil = libsUtil;
@Prop({ required: true }) password!: string;
@Prop({ default: "Your password failed. Please go back and try again." })
decryptFailureMessage!: string;
@Prop({ default: false }) showOrganizerTools!: boolean;
decryptedMembers: DecryptedMember[] = [];
missingPassword = false;
missingMyself = false;
isLoading = false;
firstName = "";
isLoading = true;
isOrganizer = false;
members: Member[] = [];
missingPassword = false;
missingMyself = false;
activeDid = "";
apiServer = "";
contacts: Array<Contact> = [];
@@ -205,13 +209,14 @@ export default class MembersList extends Vue {
const settings = await retrieveSettingsForActiveAccount();
this.activeDid = settings.activeDid || "";
this.apiServer = settings.apiServer || "";
this.firstName = settings.firstName || "";
await this.fetchMembers();
await this.loadContacts();
}
async fetchMembers() {
this.isLoading = true;
try {
this.isLoading = true;
const headers = await getHeaders(this.activeDid);
const response = await this.axios.get(
`${this.apiServer}/api/partner/groupOnboardMembers`,
@@ -274,6 +279,28 @@ export default class MembersList extends Vue {
this.missingMyself = !foundMyself;
}
decryptionErrorMessage(): string {
if (this.isOrganizer) {
if (this.decryptedMembers.length < this.members.length) {
return "Some members have data that cannot be decrypted with that password.";
} else {
// the lists must be equal
return "";
}
} else {
// non-organizers should only see problems if the first (organizer) member is not decrypted
if (
this.decryptedMembers.length === 0 ||
this.decryptedMembers[0].member.memberId !== this.members[0].memberId
) {
return "Your password is not the same as the organizer. Reload or have them check their password.";
} else {
// the first (organizer) member was decrypted OK
return "";
}
}
}
membersToShow(): DecryptedMember[] {
if (this.isOrganizer) {
if (this.showOrganizerTools) {
@@ -356,7 +383,7 @@ export default class MembersList extends Vue {
group: "modal",
type: "confirm",
title: "Continue Without Adding?",
text: "Are you sure you want to proceed with admission even though they are not a contact?",
text: "Are you sure you want to proceed with admission? If they are not a contact, you will not know their name after this meeting.",
yesText: "Continue",
onYes: async () => {
await this.toggleAdmission(decrMember);

View File

@@ -4,8 +4,7 @@
<div class="dialog">
<h1 class="text-xl font-bold text-center mb-4">Set Your Name</h1>
This is not sent to servers. It is only shared with people when you send
it to them.
{{ sharingExplanation }}
<input
type="text"
placeholder="Name"
@@ -36,7 +35,7 @@
</template>
<script lang="ts">
import { Vue, Component } from "vue-facing-decorator";
import { Vue, Component, Prop } from "vue-facing-decorator";
import { NotificationIface } from "@/constants/app";
import { db, retrieveSettingsForActiveAccount } from "@/db/index";
@@ -46,14 +45,21 @@ import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
export default class UserNameDialog extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void;
callback: (name: string) => void = () => {};
@Prop({
default:
"This is not sent to servers. It is only shared with people when you send it to them.",
})
sharingExplanation!: string;
@Prop({ default: false }) callbackOnCancel!: boolean;
callback: (name?: string) => void = () => {};
givenName = "";
visible = false;
/**
* @param aCallback - callback function for name, which may be ""
*/
async open(aCallback?: (name: string) => void) {
async open(aCallback?: (name?: string) => void) {
this.callback = aCallback || this.callback;
const settings = await retrieveSettingsForActiveAccount();
this.givenName = settings.firstName || "";
@@ -70,6 +76,9 @@ export default class UserNameDialog extends Vue {
onClickCancel() {
this.visible = false;
if (this.callbackOnCancel) {
this.callback();
}
}
}
</script>

View File

@@ -281,7 +281,7 @@
/>
</span>
</span>
<span class="col-span-10 justify-self-stretch">
<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="
@@ -315,7 +315,7 @@
/>
</span>
-->
<span class="pl-2">
<span class="pl-2 block break-words">
{{ giveDescription(record) }}
</span>
<a @click="onClickLoadClaim(record.jwtId)">
@@ -346,10 +346,18 @@
</router-link>
</span>
</div>
<div v-if="record.image" class="flex justify-center">
<a :href="record.image" target="_blank">
<img :src="record.image" class="h-48 mt-2 rounded-xl" />
</a>
<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>
@@ -368,6 +376,12 @@
</section>
<ChoiceButtonDialog ref="choiceButtonDialog" />
<ImageViewer
:image-url="selectedImage"
:image-data="selectedImageData"
v-model:is-open="isImageViewerOpen"
/>
</template>
<script lang="ts">
@@ -386,6 +400,7 @@ 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,
@@ -455,6 +470,7 @@ interface GiveRecordWithContactInfo extends GiveSummaryRecord {
QuickNav,
TopMessage,
UserNameDialog,
ImageViewer,
},
})
export default class HomeView extends Vue {
@@ -489,6 +505,10 @@ export default class HomeView extends Vue {
}> = [];
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 {
@@ -970,5 +990,21 @@ export default class HomeView extends Vue {
},
});
}
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>

View File

@@ -239,8 +239,8 @@ import {
import {
accountFromExtendedKey,
extendedKeysFromSeedWords,
} from "nostr-tools/nip06";
import { finalizeEvent, serializeEvent } from "nostr-tools/pure";
} from "nostr-tools/lib/types/nip06";
import { finalizeEvent, serializeEvent } from "nostr-tools";
import { Component, Vue } from "vue-facing-decorator";
import { LMap, LMarker, LTileLayer } from "@vue-leaflet/vue-leaflet";
import { RouteLocationNormalizedLoaded, Router } from "vue-router";

View File

@@ -8,8 +8,16 @@
Meeting Members
</h1>
<!-- Loading Animation -->
<div
class="mt-16 text-center text-4xl bg-slate-400 text-white w-14 py-2.5 rounded-full mx-auto"
v-if="isLoading"
>
<fa icon="spinner" class="fa-spin-pulse"></fa>
</div>
<!-- Error State -->
<div v-if="errorMessage">
<div v-else-if="errorMessage">
<div class="text-center text-red-600 py-8">
{{ errorMessage }}
</div>
@@ -19,13 +27,14 @@
</div>
<!-- Members List -->
<MembersList
v-else
:password="password"
:decrypt-failure-message="'That password failed. You may be in the wrong meeting. Go back and try again.'"
@error="handleError"
/>
<MembersList v-else :password="password" @error="handleError" />
</section>
<UserNameDialog
ref="userNameDialog"
:callback-on-cancel="true"
sharing-explanation="This is encrypted and shared only with people in this meeting."
/>
</template>
<script lang="ts">
@@ -35,16 +44,35 @@ import { RouteLocation } from "vue-router";
import QuickNav from "@/components/QuickNav.vue";
import TopMessage from "@/components/TopMessage.vue";
import MembersList from "@/components/MembersList.vue";
import UserNameDialog from "@/components/UserNameDialog.vue";
import { logConsoleAndDb, retrieveSettingsForActiveAccount } from "@/db/index";
import { encryptMessage } from "@/libs/crypto";
import {
errorStringForLog,
getHeaders,
serverMessageForUser,
} from "@/libs/endorserServer";
import { generateSaveAndActivateIdentity } from "@/libs/util";
@Component({
components: {
QuickNav,
TopMessage,
MembersList,
UserNameDialog,
},
})
export default class OnboardMeetingMembersView extends Vue {
activeDid = "";
apiServer = "";
errorMessage = "";
firstName = "";
isRegistered = false;
isLoading = true;
$refs!: {
userNameDialog: InstanceType<typeof UserNameDialog>;
};
get groupId(): string {
return (this.$route as RouteLocation).params.groupId as string;
@@ -63,6 +91,122 @@ export default class OnboardMeetingMembersView extends Vue {
this.errorMessage = "The password is missing. Go back and try again.";
return;
}
const settings = await retrieveSettingsForActiveAccount();
this.activeDid = settings.activeDid || "";
this.apiServer = settings.apiServer || "";
this.firstName = settings.firstName || "";
this.isRegistered = settings.isRegistered || false;
try {
if (!this.activeDid) {
this.activeDid = await generateSaveAndActivateIdentity();
this.isRegistered = false;
}
const headers = await getHeaders(this.activeDid);
const response = await this.axios.get(
`${this.apiServer}/api/partner/groupOnboardMember`,
{ headers },
);
const member = response.data?.data;
if (!member) {
if (!this.firstName) {
this.$refs.userNameDialog.open(this.addMemberToMeeting);
// addMemberToMeeting sets isLoading to false
} else {
await this.addMemberToMeeting(this.firstName);
// addMemberToMeeting sets isLoading to false
}
} else if (String(member.groupId) !== this.groupId) {
this.errorMessage =
"You are already in a different meeting. Reload or go back and try again.";
this.isLoading = false;
} else {
// must be already in the right meeting
if (!this.firstName) {
this.$refs.userNameDialog.open(this.updateMemberInMeeting);
// updateMemberInMeeting sets isLoading to false
} else {
await this.updateMemberInMeeting(this.firstName);
// updateMemberInMeeting sets isLoading to false
}
}
} catch (error) {
this.errorMessage =
serverMessageForUser(error) ||
"There was an error checking for that meeting. Reload or go back and try again.";
logConsoleAndDb(
"Error checking meeting: " + errorStringForLog(error),
true,
);
this.isLoading = false;
}
}
async addMemberToMeeting(name?: string) {
if (name != null) {
this.firstName = name;
}
const memberData = {
name: this.firstName,
did: this.activeDid,
isRegistered: this.isRegistered,
};
const memberDataString = JSON.stringify(memberData);
const encryptedMemberData = await encryptMessage(
memberDataString,
this.password,
);
const headers = await getHeaders(this.activeDid);
try {
await this.axios.post(
`${this.apiServer}/api/partner/groupOnboardMember`,
{ groupId: this.groupId, content: encryptedMemberData },
{ headers },
);
} catch (error) {
logConsoleAndDb(
"Error adding member to meeting: " + errorStringForLog(error),
true,
);
this.errorMessage =
serverMessageForUser(error) ||
"You're not in a meeting and couldn't be added to this one. Reload or go back and try again.";
}
this.isLoading = false;
}
async updateMemberInMeeting(name?: string) {
if (name != null) {
this.firstName = name;
}
const memberData = {
name: this.firstName,
did: this.activeDid,
isRegistered: this.isRegistered,
};
const memberDataString = JSON.stringify(memberData);
const encryptedMemberData = await encryptMessage(
memberDataString,
this.password,
);
const headers = await getHeaders(this.activeDid);
try {
await this.axios.put(
`${this.apiServer}/api/partner/groupOnboardMember`,
{ content: encryptedMemberData },
{ headers },
);
} catch (error) {
logConsoleAndDb(
"Error updating member in meeting: " + errorStringForLog(error),
true,
);
this.errorMessage =
serverMessageForUser(error) ||
"There was an error updating your name. Reload or go back and try again.";
}
this.isLoading = false;
}
handleError(message: string) {

View File

@@ -192,23 +192,23 @@
<!-- Members Section -->
<div
v-if="!isLoading && currentMeeting != null"
v-if="!isLoading && currentMeeting != null && !!currentMeeting.password"
class="mt-8 p-4 border rounded-lg bg-white shadow"
>
<div class="flex items-center justify-between mb-4">
<h2 class="text-2xl">Meeting Members</h2>
</div>
<router-link
v-if="!!currentMeeting.password"
:to="onboardMeetingMembersLink()"
class="inline-block text-blue-600"
target="_blank"
>
Open shortcut page for members <fa icon="external-link" />
&bull; Open shortcut page for members <fa icon="external-link" />
</router-link>
<MembersList
:password="currentMeeting.password || ''"
:decrypt-failure-message="DECRYPT_FAILURE_MESSAGE"
:show-organizer-tools="true"
@error="handleMembersError"
class="mt-4"
@@ -264,9 +264,6 @@ export default class OnboardMeetingView extends Vue {
timeout?: number,
) => void;
DECRYPT_FAILURE_MESSAGE =
"Unable to decrypt some member information. Check your password, or have them reset theirs if they don't show here.";
currentMeeting: ServerMeeting | null = null;
newOrUpdatedMeeting: MeetingSetupInfo | null = null;
activeDid = "";