Merge branch 'master' into split_build_process

fix: image server references and test configurations

- Update image server references to use test server by default for local dev
- Fix registration status checks in tests
- Remove verbose console logging
- Update environment configurations for consistent image server usage
- Fix alert handling in contact registration tests
- Clean up component lifecycle logging
- Add clarifying comments about shared image server usage
- Update playwright test configurations for better reliability

This commit ensures consistent image server behavior across environments
and improves test reliability by properly handling registration status
checks and alerts.
This commit is contained in:
Matthew Raymer
2025-02-17 06:36:40 +00:00
19 changed files with 633 additions and 147 deletions

View File

@@ -66,7 +66,7 @@
<script lang="ts">
import { Component, Vue } from "vue-facing-decorator";
import { NotificationIface } from "@/constants/app";
import { NotificationIface } from "../constants/app";
@Component
export default class PromptDialog extends Vue {

View File

@@ -0,0 +1,94 @@
<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"
@@ -171,13 +176,13 @@ interface Member {
admitted: boolean;
content: string;
memberId: number;
registered: boolean;
}
interface DecryptedMember {
member: Member;
name: string;
did: string;
isRegistered: boolean;
}
@Component
@@ -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`,
@@ -258,6 +263,7 @@ export default class MembersList extends Vue {
member: member,
name: content.name,
did: content.did,
isRegistered: !!content.isRegistered,
});
if (isFirstEntry && content.did === this.activeDid) {
this.isOrganizer = true;
@@ -273,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) {
@@ -293,7 +321,7 @@ export default class MembersList extends Vue {
group: "alert",
type: "info",
title: "Admission info",
text: "This is to register people and admit them to the meeting. A '+' symbol means they are not yet admitted and you can register and admit them. A '-' means you can remove them, but they will stay registered.",
text: "This is to register people in Time Safari and to admit them to the meeting. A '+' symbol means they are not yet admitted and you can register and admit them. A '-' means you can remove them, but they will stay registered.",
},
10000,
);
@@ -331,9 +359,9 @@ export default class MembersList extends Vue {
return this.contacts.find((contact) => contact.did === did);
}
checkWhetherContactBeforeAdmitting(member: DecryptedMember) {
const contact = this.getContactFor(member.did);
if (!member.member.admitted && !contact) {
checkWhetherContactBeforeAdmitting(decrMember: DecryptedMember) {
const contact = this.getContactFor(decrMember.did);
if (!decrMember.member.admitted && !contact) {
// If not a contact, show confirmation dialog
this.$notify(
{
@@ -344,9 +372,9 @@ export default class MembersList extends Vue {
yesText: "Add as Contact",
noText: "Skip Adding Contact",
onYes: async () => {
await this.addAsContact(member);
await this.addAsContact(decrMember);
// After adding as contact, proceed with admission
await this.toggleAdmission(member);
await this.toggleAdmission(decrMember);
},
onNo: async () => {
// If they choose not to add as contact, show second confirmation
@@ -355,10 +383,10 @@ 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(member);
await this.toggleAdmission(decrMember);
},
onCancel: async () => {
// Do nothing, effectively canceling the operation
@@ -372,61 +400,72 @@ export default class MembersList extends Vue {
);
} else {
// If already a contact, proceed directly with admission
this.toggleAdmission(member);
this.toggleAdmission(decrMember);
}
}
async toggleAdmission(member: DecryptedMember) {
async toggleAdmission(decrMember: DecryptedMember) {
try {
const headers = await getHeaders(this.activeDid);
await this.axios.put(
`${this.apiServer}/api/partner/groupOnboardMember/${member.member.memberId}`,
{ admitted: !member.member.admitted },
`${this.apiServer}/api/partner/groupOnboardMember/${decrMember.member.memberId}`,
{ admitted: !decrMember.member.admitted },
{ headers },
);
// Update local state
member.member.admitted = !member.member.admitted;
decrMember.member.admitted = !decrMember.member.admitted;
const oldContact = this.getContactFor(member.did);
const oldContact = this.getContactFor(decrMember.did);
// if admitted, now register that user if they are not registered
if (member.member.admitted && !oldContact?.registered) {
if (
decrMember.member.admitted &&
!decrMember.isRegistered &&
!oldContact?.registered
) {
const contactOldOrNew: Contact = oldContact || {
did: member.did,
name: member.name,
did: decrMember.did,
name: decrMember.name,
};
const result = await register(
this.activeDid,
this.apiServer,
this.axios,
contactOldOrNew,
);
if (result.success) {
member.member.registered = true;
if (oldContact) {
await db.contacts.update(member.did, { registered: true });
oldContact.registered = true;
}
this.$notify(
{
group: "alert",
type: "success",
title: "Registered",
text: "Besides being admitted, they were also registered.",
},
3000,
try {
const result = await register(
this.activeDid,
this.apiServer,
this.axios,
contactOldOrNew,
);
} else {
const additionalInfo = result.error || "";
if (result.success) {
decrMember.isRegistered = true;
if (oldContact) {
await db.contacts.update(decrMember.did, { registered: true });
oldContact.registered = true;
}
this.$notify(
{
group: "alert",
type: "success",
title: "Registered",
text: "Besides being admitted, they were also registered.",
},
3000,
);
} else {
throw result;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (error: any) {
// registration failure is likely explained by a message from the server
const additionalInfo =
serverMessageForUser(error) || error?.error || "";
this.$notify(
{
group: "alert",
type: "danger",
type: "warning",
title: "Registration failed",
text:
"They were admitted, but registration failed. You can try again, or register from your contacts screen. " +
"They were admitted to the meeting. However, registration failed. You can register them from the contacts screen. " +
additionalInfo,
},
10000,
12000,
);
}
}

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>