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:
38
CHANGELOG.md
38
CHANGELOG.md
@@ -6,6 +6,44 @@ 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).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
|
||||||
|
== [0.4.2] - 2025.02.17
|
||||||
|
### Merged
|
||||||
|
- Master to split_process_build
|
||||||
|
- fixed path issues
|
||||||
|
- all tests passing
|
||||||
|
- capacitor build to Android working
|
||||||
|
|
||||||
|
## [0.4.1] - 2025.02.16
|
||||||
|
### Fixed
|
||||||
|
- nostr build issue
|
||||||
|
- Linting
|
||||||
|
|
||||||
|
|
||||||
|
## [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
|
||||||
|
|
||||||
|
|
||||||
|
## [0.3.54] - 2025.02.06
|
||||||
|
### Added
|
||||||
|
- Group onboarding meetings
|
||||||
|
|
||||||
|
|
||||||
## [0.3.53] - 2025.01.30
|
## [0.3.53] - 2025.01.30
|
||||||
### Added
|
### Added
|
||||||
- Hints for contacting the creator of a project
|
- Hints for contacting the creator of a project
|
||||||
|
|||||||
59
README.md
59
README.md
@@ -19,7 +19,64 @@ npm install
|
|||||||
npm run dev
|
npm run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
See [BUILDING.md](BUILDING.md) for detailed build and setup instructions.
|
See the test locations for "IMAGE_API_SERVER" or "PARTNER_API_SERVER" below, or use http://localhost:3000 for local endorser.ch
|
||||||
|
|
||||||
|
### Build the test & production app
|
||||||
|
```
|
||||||
|
npm run serve
|
||||||
|
```
|
||||||
|
|
||||||
|
### Lint and fix files
|
||||||
|
```
|
||||||
|
npm run lint
|
||||||
|
```
|
||||||
|
|
||||||
|
### Run all UI tests
|
||||||
|
|
||||||
|
Look below for the "test-all" instructions.
|
||||||
|
|
||||||
|
|
||||||
|
### Compile and minify for test & production
|
||||||
|
|
||||||
|
* If there are DB changes: before updating the test server, open browser(s) with current version to test DB migrations.
|
||||||
|
|
||||||
|
* `npx prettier --write ./sw_scripts/`
|
||||||
|
|
||||||
|
* Update the ClickUp tasks & CHANGELOG.md & the version in package.json, run `npm install`.
|
||||||
|
|
||||||
|
* Commit everything (since the commit hash is used the app).
|
||||||
|
|
||||||
|
* 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.55 && git push origin 0.3.55`.
|
||||||
|
|
||||||
|
* For test, build the app (because test server is not yet set up to build):
|
||||||
|
|
||||||
|
```
|
||||||
|
TIME_SAFARI_APP_TITLE="TimeSafari_Test" VITE_APP_SERVER=https://test.timesafari.app VITE_BVC_MEETUPS_PROJECT_CLAIM_ID=https://endorser.ch/entity/01HWE8FWHQ1YGP7GFZYYPS272F VITE_DEFAULT_ENDORSER_API_SERVER=https://test-api.endorser.ch VITE_DEFAULT_IMAGE_API_SERVER=https://test-image-api.timesafari.app VITE_DEFAULT_PARTNER_API_SERVER=https://test-partner-api.endorser.ch VITE_PASSKEYS_ENABLED=true npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
... and transfer to the test server: `rsync -azvu -e "ssh -i ~/.ssh/..." dist ubuntutest@test.timesafari.app:time-safari`
|
||||||
|
|
||||||
|
(Let's replace that with a .env.development or .env.staging file.)
|
||||||
|
|
||||||
|
(Note: The test BVC_MEETUPS_PROJECT_CLAIM_ID does not resolve as a URL because it's only in the test DB and the prod redirect won't redirect there.)
|
||||||
|
|
||||||
|
* For prod, get on the server and run the correct build:
|
||||||
|
|
||||||
|
... and log onto the server:
|
||||||
|
|
||||||
|
* `pkgx +npm sh`
|
||||||
|
|
||||||
|
* `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.)
|
||||||
|
|
||||||
|
* Back up the time-safari/dist folder & deploy: `mv time-safari/dist time-safari-dist-prev.0 && mv crowd-funder-for-time-pwa/dist time-safari/`
|
||||||
|
|
||||||
|
* Record the new hash in the changelog. Edit package.json to increment version & add "-beta", `npm install`, and commit. Also record what version is on production.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ buildscript {
|
|||||||
mavenCentral()
|
mavenCentral()
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath 'com.android.tools.build:gradle:8.8.0'
|
classpath 'com.android.tools.build:gradle:8.8.1'
|
||||||
classpath 'com.google.gms:google-services:4.4.0'
|
classpath 'com.google.gms:google-services:4.4.0'
|
||||||
|
|
||||||
// NOTE: Do not place your application dependencies here; they belong
|
// NOTE: Do not place your application dependencies here; they belong
|
||||||
|
|||||||
6
package-lock.json
generated
6
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "TimeSafari",
|
"name": "TimeSafari",
|
||||||
"version": "0.3.54-beta",
|
"version": "0.4.1",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "TimeSafari",
|
"name": "TimeSafari",
|
||||||
"version": "0.3.54-beta",
|
"version": "0.4.1",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@capacitor/android": "^6.2.0",
|
"@capacitor/android": "^6.2.0",
|
||||||
"@capacitor/cli": "^6.2.0",
|
"@capacitor/cli": "^6.2.0",
|
||||||
@@ -54,7 +54,7 @@
|
|||||||
"lru-cache": "^10.2.0",
|
"lru-cache": "^10.2.0",
|
||||||
"luxon": "^3.4.4",
|
"luxon": "^3.4.4",
|
||||||
"merkletreejs": "^0.3.11",
|
"merkletreejs": "^0.3.11",
|
||||||
"nostr-tools": "^2.7.2",
|
"nostr-tools": "^2.10.4",
|
||||||
"notiwind": "^2.0.2",
|
"notiwind": "^2.0.2",
|
||||||
"papaparse": "^5.4.1",
|
"papaparse": "^5.4.1",
|
||||||
"pina": "^0.20.2204228",
|
"pina": "^0.20.2204228",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "TimeSafari",
|
"name": "timesafari",
|
||||||
"version": "0.3.54-beta",
|
"version": "0.4.2",
|
||||||
"description": "TimeSafari Desktop Application",
|
"description": "TimeSafari Desktop Application",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "TimeSafari Team"
|
"name": "TimeSafari Team"
|
||||||
@@ -77,7 +77,7 @@
|
|||||||
"lru-cache": "^10.2.0",
|
"lru-cache": "^10.2.0",
|
||||||
"luxon": "^3.4.4",
|
"luxon": "^3.4.4",
|
||||||
"merkletreejs": "^0.3.11",
|
"merkletreejs": "^0.3.11",
|
||||||
"nostr-tools": "^2.7.2",
|
"nostr-tools": "^2.10.4",
|
||||||
"notiwind": "^2.0.2",
|
"notiwind": "^2.0.2",
|
||||||
"papaparse": "^5.4.1",
|
"papaparse": "^5.4.1",
|
||||||
"pina": "^0.20.2204228",
|
"pina": "^0.20.2204228",
|
||||||
|
|||||||
@@ -66,7 +66,7 @@
|
|||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Vue } from "vue-facing-decorator";
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
import { NotificationIface } from "@/constants/app";
|
import { NotificationIface } from "../constants/app";
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
export default class PromptDialog extends Vue {
|
export default class PromptDialog extends Vue {
|
||||||
|
|||||||
94
src/components/ImageViewer.vue
Normal file
94
src/components/ImageViewer.vue
Normal 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>
|
||||||
@@ -1,40 +1,41 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<!-- Loading State -->
|
<!-- 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" />
|
<fa icon="spinner" class="fa-spin-pulse" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Members List -->
|
<!-- Members List -->
|
||||||
|
|
||||||
<p
|
<div v-else>
|
||||||
v-if="decryptedMembers.length < members.length"
|
<div class="text-center text-red-600 py-4">
|
||||||
class="text-center text-red-600 py-4"
|
{{ decryptionErrorMessage() }}
|
||||||
>
|
</div>
|
||||||
{{
|
|
||||||
decryptFailureMessage ||
|
|
||||||
"Your password failed. Please go back and try again."
|
|
||||||
}}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div v-else class="space-y-4">
|
|
||||||
<div v-if="missingMyself" class="py-4 text-red-600">
|
<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>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<span
|
<span
|
||||||
v-if="showOrganizerTools && isOrganizer"
|
v-if="membersToShow().length > 0 && showOrganizerTools && isOrganizer"
|
||||||
class="inline-flex items-center flex-wrap"
|
class="inline-flex items-center flex-wrap"
|
||||||
>
|
>
|
||||||
<span class="inline-flex items-center">
|
<span class="inline-flex items-center">
|
||||||
Use
|
• Click
|
||||||
<span
|
<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"
|
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" />
|
<fa icon="plus" class="text-sm" />
|
||||||
</span>
|
</span>
|
||||||
and
|
/
|
||||||
<span
|
<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"
|
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>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span class="inline-flex items-center">
|
<span
|
||||||
Use
|
v-if="membersToShow().length > 0"
|
||||||
|
class="inline-flex items-center"
|
||||||
|
>
|
||||||
|
• Click
|
||||||
<span
|
<span
|
||||||
class="mx-2 w-8 h-8 flex items-center justify-center rounded-full bg-green-100 text-green-600"
|
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>
|
</span>
|
||||||
</div>
|
</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
|
<button
|
||||||
@click="fetchMembers"
|
@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"
|
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
|
<div
|
||||||
v-for="member in membersToShow()"
|
v-for="member in membersToShow()"
|
||||||
:key="member.member.memberId"
|
: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 justify-between">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
@@ -131,7 +136,7 @@
|
|||||||
{{ member.did }}
|
{{ member.did }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</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
|
<button
|
||||||
@click="fetchMembers"
|
@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"
|
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;
|
admitted: boolean;
|
||||||
content: string;
|
content: string;
|
||||||
memberId: number;
|
memberId: number;
|
||||||
registered: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DecryptedMember {
|
interface DecryptedMember {
|
||||||
member: Member;
|
member: Member;
|
||||||
name: string;
|
name: string;
|
||||||
did: string;
|
did: string;
|
||||||
|
isRegistered: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
@@ -187,16 +192,15 @@ export default class MembersList extends Vue {
|
|||||||
libsUtil = libsUtil;
|
libsUtil = libsUtil;
|
||||||
|
|
||||||
@Prop({ required: true }) password!: string;
|
@Prop({ required: true }) password!: string;
|
||||||
@Prop({ default: "Your password failed. Please go back and try again." })
|
|
||||||
decryptFailureMessage!: string;
|
|
||||||
@Prop({ default: false }) showOrganizerTools!: boolean;
|
@Prop({ default: false }) showOrganizerTools!: boolean;
|
||||||
|
|
||||||
decryptedMembers: DecryptedMember[] = [];
|
decryptedMembers: DecryptedMember[] = [];
|
||||||
missingPassword = false;
|
firstName = "";
|
||||||
missingMyself = false;
|
isLoading = true;
|
||||||
isLoading = false;
|
|
||||||
isOrganizer = false;
|
isOrganizer = false;
|
||||||
members: Member[] = [];
|
members: Member[] = [];
|
||||||
|
missingPassword = false;
|
||||||
|
missingMyself = false;
|
||||||
activeDid = "";
|
activeDid = "";
|
||||||
apiServer = "";
|
apiServer = "";
|
||||||
contacts: Array<Contact> = [];
|
contacts: Array<Contact> = [];
|
||||||
@@ -205,13 +209,14 @@ export default class MembersList extends Vue {
|
|||||||
const settings = await retrieveSettingsForActiveAccount();
|
const settings = await retrieveSettingsForActiveAccount();
|
||||||
this.activeDid = settings.activeDid || "";
|
this.activeDid = settings.activeDid || "";
|
||||||
this.apiServer = settings.apiServer || "";
|
this.apiServer = settings.apiServer || "";
|
||||||
|
this.firstName = settings.firstName || "";
|
||||||
await this.fetchMembers();
|
await this.fetchMembers();
|
||||||
await this.loadContacts();
|
await this.loadContacts();
|
||||||
}
|
}
|
||||||
|
|
||||||
async fetchMembers() {
|
async fetchMembers() {
|
||||||
this.isLoading = true;
|
|
||||||
try {
|
try {
|
||||||
|
this.isLoading = true;
|
||||||
const headers = await getHeaders(this.activeDid);
|
const headers = await getHeaders(this.activeDid);
|
||||||
const response = await this.axios.get(
|
const response = await this.axios.get(
|
||||||
`${this.apiServer}/api/partner/groupOnboardMembers`,
|
`${this.apiServer}/api/partner/groupOnboardMembers`,
|
||||||
@@ -258,6 +263,7 @@ export default class MembersList extends Vue {
|
|||||||
member: member,
|
member: member,
|
||||||
name: content.name,
|
name: content.name,
|
||||||
did: content.did,
|
did: content.did,
|
||||||
|
isRegistered: !!content.isRegistered,
|
||||||
});
|
});
|
||||||
if (isFirstEntry && content.did === this.activeDid) {
|
if (isFirstEntry && content.did === this.activeDid) {
|
||||||
this.isOrganizer = true;
|
this.isOrganizer = true;
|
||||||
@@ -273,6 +279,28 @@ export default class MembersList extends Vue {
|
|||||||
this.missingMyself = !foundMyself;
|
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[] {
|
membersToShow(): DecryptedMember[] {
|
||||||
if (this.isOrganizer) {
|
if (this.isOrganizer) {
|
||||||
if (this.showOrganizerTools) {
|
if (this.showOrganizerTools) {
|
||||||
@@ -293,7 +321,7 @@ export default class MembersList extends Vue {
|
|||||||
group: "alert",
|
group: "alert",
|
||||||
type: "info",
|
type: "info",
|
||||||
title: "Admission 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,
|
10000,
|
||||||
);
|
);
|
||||||
@@ -331,9 +359,9 @@ export default class MembersList extends Vue {
|
|||||||
return this.contacts.find((contact) => contact.did === did);
|
return this.contacts.find((contact) => contact.did === did);
|
||||||
}
|
}
|
||||||
|
|
||||||
checkWhetherContactBeforeAdmitting(member: DecryptedMember) {
|
checkWhetherContactBeforeAdmitting(decrMember: DecryptedMember) {
|
||||||
const contact = this.getContactFor(member.did);
|
const contact = this.getContactFor(decrMember.did);
|
||||||
if (!member.member.admitted && !contact) {
|
if (!decrMember.member.admitted && !contact) {
|
||||||
// If not a contact, show confirmation dialog
|
// If not a contact, show confirmation dialog
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
@@ -344,9 +372,9 @@ export default class MembersList extends Vue {
|
|||||||
yesText: "Add as Contact",
|
yesText: "Add as Contact",
|
||||||
noText: "Skip Adding Contact",
|
noText: "Skip Adding Contact",
|
||||||
onYes: async () => {
|
onYes: async () => {
|
||||||
await this.addAsContact(member);
|
await this.addAsContact(decrMember);
|
||||||
// After adding as contact, proceed with admission
|
// After adding as contact, proceed with admission
|
||||||
await this.toggleAdmission(member);
|
await this.toggleAdmission(decrMember);
|
||||||
},
|
},
|
||||||
onNo: async () => {
|
onNo: async () => {
|
||||||
// If they choose not to add as contact, show second confirmation
|
// If they choose not to add as contact, show second confirmation
|
||||||
@@ -355,10 +383,10 @@ export default class MembersList extends Vue {
|
|||||||
group: "modal",
|
group: "modal",
|
||||||
type: "confirm",
|
type: "confirm",
|
||||||
title: "Continue Without Adding?",
|
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",
|
yesText: "Continue",
|
||||||
onYes: async () => {
|
onYes: async () => {
|
||||||
await this.toggleAdmission(member);
|
await this.toggleAdmission(decrMember);
|
||||||
},
|
},
|
||||||
onCancel: async () => {
|
onCancel: async () => {
|
||||||
// Do nothing, effectively canceling the operation
|
// Do nothing, effectively canceling the operation
|
||||||
@@ -372,61 +400,72 @@ export default class MembersList extends Vue {
|
|||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
// If already a contact, proceed directly with admission
|
// If already a contact, proceed directly with admission
|
||||||
this.toggleAdmission(member);
|
this.toggleAdmission(decrMember);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async toggleAdmission(member: DecryptedMember) {
|
async toggleAdmission(decrMember: DecryptedMember) {
|
||||||
try {
|
try {
|
||||||
const headers = await getHeaders(this.activeDid);
|
const headers = await getHeaders(this.activeDid);
|
||||||
await this.axios.put(
|
await this.axios.put(
|
||||||
`${this.apiServer}/api/partner/groupOnboardMember/${member.member.memberId}`,
|
`${this.apiServer}/api/partner/groupOnboardMember/${decrMember.member.memberId}`,
|
||||||
{ admitted: !member.member.admitted },
|
{ admitted: !decrMember.member.admitted },
|
||||||
{ headers },
|
{ headers },
|
||||||
);
|
);
|
||||||
// Update local state
|
// 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 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 || {
|
const contactOldOrNew: Contact = oldContact || {
|
||||||
did: member.did,
|
did: decrMember.did,
|
||||||
name: member.name,
|
name: decrMember.name,
|
||||||
};
|
};
|
||||||
const result = await register(
|
try {
|
||||||
this.activeDid,
|
const result = await register(
|
||||||
this.apiServer,
|
this.activeDid,
|
||||||
this.axios,
|
this.apiServer,
|
||||||
contactOldOrNew,
|
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,
|
|
||||||
);
|
);
|
||||||
} else {
|
if (result.success) {
|
||||||
const additionalInfo = result.error || "";
|
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(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: "alert",
|
group: "alert",
|
||||||
type: "danger",
|
type: "warning",
|
||||||
title: "Registration failed",
|
title: "Registration failed",
|
||||||
text:
|
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,
|
additionalInfo,
|
||||||
},
|
},
|
||||||
10000,
|
12000,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,8 +4,7 @@
|
|||||||
<div class="dialog">
|
<div class="dialog">
|
||||||
<h1 class="text-xl font-bold text-center mb-4">Set Your Name</h1>
|
<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
|
{{ sharingExplanation }}
|
||||||
it to them.
|
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Name"
|
placeholder="Name"
|
||||||
@@ -36,7 +35,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Vue, Component } from "vue-facing-decorator";
|
import { Vue, Component, Prop } from "vue-facing-decorator";
|
||||||
|
|
||||||
import { NotificationIface } from "../constants/app";
|
import { NotificationIface } from "../constants/app";
|
||||||
import { db, retrieveSettingsForActiveAccount } from "../db/index";
|
import { db, retrieveSettingsForActiveAccount } from "../db/index";
|
||||||
@@ -46,14 +45,21 @@ import { MASTER_SETTINGS_KEY } from "../db/tables/settings";
|
|||||||
export default class UserNameDialog extends Vue {
|
export default class UserNameDialog extends Vue {
|
||||||
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
$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 = "";
|
givenName = "";
|
||||||
visible = false;
|
visible = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param aCallback - callback function for name, which may be ""
|
* @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;
|
this.callback = aCallback || this.callback;
|
||||||
const settings = await retrieveSettingsForActiveAccount();
|
const settings = await retrieveSettingsForActiveAccount();
|
||||||
this.givenName = settings.firstName || "";
|
this.givenName = settings.firstName || "";
|
||||||
@@ -70,6 +76,9 @@ export default class UserNameDialog extends Vue {
|
|||||||
|
|
||||||
onClickCancel() {
|
onClickCancel() {
|
||||||
this.visible = false;
|
this.visible = false;
|
||||||
|
if (this.callbackOnCancel) {
|
||||||
|
this.callback();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import * as didJwt from "did-jwt";
|
import * as didJwt from "did-jwt";
|
||||||
import { AppString } from "@/constants/app";
|
import { AppString } from "../constants/app";
|
||||||
import { retrieveSettingsForActiveAccount } from "../db";
|
import { retrieveSettingsForActiveAccount } from "../db";
|
||||||
import { SERVICE_ID } from "@/libs/endorserServer";
|
import { SERVICE_ID } from "../libs/endorserServer";
|
||||||
import { deriveAddress, newIdentifier } from "@/libs/crypto";
|
import { deriveAddress, newIdentifier } from "../libs/crypto";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get User #0 to sign & submit a RegisterAction for the user's activeDid.
|
* Get User #0 to sign & submit a RegisterAction for the user's activeDid.
|
||||||
|
|||||||
@@ -21,9 +21,9 @@ import { Component, Vue } from "vue-facing-decorator";
|
|||||||
import { nextTick } from "vue";
|
import { nextTick } from "vue";
|
||||||
import QRCode from "qrcode";
|
import QRCode from "qrcode";
|
||||||
|
|
||||||
import { APP_SERVER, NotificationIface } from "@/constants/app";
|
import { APP_SERVER, NotificationIface } from "../constants/app";
|
||||||
import { db, retrieveSettingsForActiveAccount } from "@/db/index";
|
import { db, retrieveSettingsForActiveAccount } from "../db/index";
|
||||||
import * as endorserServer from "@/libs/endorserServer";
|
import * as endorserServer from "../libs/endorserServer";
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
export default class ClaimReportCertificateView extends Vue {
|
export default class ClaimReportCertificateView extends Vue {
|
||||||
|
|||||||
@@ -74,6 +74,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<fa icon="calendar" class="fa-fw text-slate-400" />
|
<fa icon="calendar" class="fa-fw text-slate-400" />
|
||||||
|
Recorded
|
||||||
{{ veriClaim.issuedAt?.replace(/T/, " ").replace(/Z/, " UTC") }}
|
{{ veriClaim.issuedAt?.replace(/T/, " ").replace(/Z/, " UTC") }}
|
||||||
</div>
|
</div>
|
||||||
<div v-if="veriClaim.claim.image" class="flex justify-center">
|
<div v-if="veriClaim.claim.image" class="flex justify-center">
|
||||||
|
|||||||
@@ -1416,11 +1416,11 @@ export default class ContactsView extends Vue {
|
|||||||
type: "confirm",
|
type: "confirm",
|
||||||
title: "Onboarding Meeting",
|
title: "Onboarding Meeting",
|
||||||
text: "Would you like to start a new meeting?",
|
text: "Would you like to start a new meeting?",
|
||||||
onYes: () => {
|
onYes: async () => {
|
||||||
(this.$router as Router).push({ name: "onboard-meeting-setup" });
|
(this.$router as Router).push({ name: "onboard-meeting-setup" });
|
||||||
},
|
},
|
||||||
yesText: "Start New Meeting",
|
yesText: "Start New Meeting",
|
||||||
onNo: () => {
|
onNo: async () => {
|
||||||
(this.$router as Router).push({ name: "onboard-meeting-list" });
|
(this.$router as Router).push({ name: "onboard-meeting-list" });
|
||||||
},
|
},
|
||||||
noText: "Join Existing Meeting",
|
noText: "Join Existing Meeting",
|
||||||
|
|||||||
@@ -281,7 +281,7 @@
|
|||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
</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
|
<!-- show giver and/or receiver profiles... which seemed like a good idea but actually adds clutter
|
||||||
<span
|
<span
|
||||||
v-if="
|
v-if="
|
||||||
@@ -315,7 +315,7 @@
|
|||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
-->
|
-->
|
||||||
<span class="pl-2">
|
<span class="pl-2 block break-words">
|
||||||
{{ giveDescription(record) }}
|
{{ giveDescription(record) }}
|
||||||
</span>
|
</span>
|
||||||
<a @click="onClickLoadClaim(record.jwtId)">
|
<a @click="onClickLoadClaim(record.jwtId)">
|
||||||
@@ -346,10 +346,18 @@
|
|||||||
</router-link>
|
</router-link>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="record.image" class="flex justify-center">
|
<div v-if="record.image" class="w-full">
|
||||||
<a :href="record.image" target="_blank">
|
<div
|
||||||
<img :src="record.image" class="h-48 mt-2 rounded-xl" />
|
class="cursor-pointer"
|
||||||
</a>
|
@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>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
@@ -368,6 +376,12 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<ChoiceButtonDialog ref="choiceButtonDialog" />
|
<ChoiceButtonDialog ref="choiceButtonDialog" />
|
||||||
|
|
||||||
|
<ImageViewer
|
||||||
|
:image-url="selectedImage"
|
||||||
|
:image-data="selectedImageData"
|
||||||
|
v-model:is-open="isImageViewerOpen"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
@@ -386,6 +400,7 @@ import QuickNav from "../components/QuickNav.vue";
|
|||||||
import TopMessage from "../components/TopMessage.vue";
|
import TopMessage from "../components/TopMessage.vue";
|
||||||
import UserNameDialog from "../components/UserNameDialog.vue";
|
import UserNameDialog from "../components/UserNameDialog.vue";
|
||||||
import ChoiceButtonDialog from "../components/ChoiceButtonDialog.vue";
|
import ChoiceButtonDialog from "../components/ChoiceButtonDialog.vue";
|
||||||
|
import ImageViewer from "../components/ImageViewer.vue";
|
||||||
import {
|
import {
|
||||||
AppString,
|
AppString,
|
||||||
NotificationIface,
|
NotificationIface,
|
||||||
@@ -450,6 +465,7 @@ interface GiveRecordWithContactInfo extends GiveSummaryRecord {
|
|||||||
QuickNav,
|
QuickNav,
|
||||||
TopMessage,
|
TopMessage,
|
||||||
UserNameDialog,
|
UserNameDialog,
|
||||||
|
ImageViewer,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
export default class HomeView extends Vue {
|
export default class HomeView extends Vue {
|
||||||
@@ -485,6 +501,10 @@ export default class HomeView extends Vue {
|
|||||||
}> = [];
|
}> = [];
|
||||||
showShortcutBvc = false;
|
showShortcutBvc = false;
|
||||||
userAgentInfo = new UAParser(); // see https://docs.uaparser.js.org/v2/api/ua-parser-js/get-os.html
|
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() {
|
async mounted() {
|
||||||
try {
|
try {
|
||||||
@@ -966,5 +986,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>
|
</script>
|
||||||
|
|||||||
@@ -71,17 +71,17 @@
|
|||||||
|
|
||||||
<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 px-3 py-2"
|
||||||
rows="5"
|
rows="5"
|
||||||
v-model="fullClaim.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">
|
||||||
If you want to be contacted, be sure to include your contact information
|
If you want to be contacted, be sure to include your contact information
|
||||||
-- just remember that this information is public and saved in a public
|
-- just remember that this information is public and saved in a public
|
||||||
history.
|
history.
|
||||||
</div>
|
</div>
|
||||||
<div class="text-xs text-slate-500 italic -mt-3 mb-4">
|
<div class="text-xs text-slate-500 italic">
|
||||||
{{ fullClaim.description?.length }}/5000 max. characters
|
{{ fullClaim.description?.length }}/5000 max. characters
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -89,28 +89,55 @@
|
|||||||
v-model="fullClaim.url"
|
v-model="fullClaim.url"
|
||||||
placeholder="Website"
|
placeholder="Website"
|
||||||
autocapitalize="none"
|
autocapitalize="none"
|
||||||
class="block w-full rounded border border-slate-400 mb-4 px-3 py-2"
|
class="block w-full rounded border border-slate-400 mt-4 px-3 py-2"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="flex mb-4 columns-3 w-full">
|
<div>
|
||||||
<input
|
<div class="flex items-center mt-4">
|
||||||
v-model="startDateInput"
|
<span class="mr-2">Starts At</span>
|
||||||
placeholder="Start Date"
|
<input
|
||||||
type="date"
|
v-model="startDateInput"
|
||||||
class="col-span-1 w-full rounded border border-slate-400 px-3 py-2"
|
placeholder="Start Date"
|
||||||
/>
|
type="date"
|
||||||
<input
|
class="rounded border border-slate-400 px-3 py-2"
|
||||||
:disabled="!startDateInput"
|
/>
|
||||||
placeholder="Start Time"
|
<input
|
||||||
v-model="startTimeInput"
|
:disabled="!startDateInput"
|
||||||
type="time"
|
placeholder="Start Time"
|
||||||
class="col-span-1 w-full rounded border border-slate-400 ml-2 px-3 py-2"
|
v-model="startTimeInput"
|
||||||
/>
|
type="time"
|
||||||
<span class="col-span-1 w-full flex justify-center">{{ zoneName }}</span>
|
class="rounded border border-slate-400 ml-2 px-3 py-2"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex w-full justify-end items-center">
|
||||||
|
<span class="w-full flex justify-end items-center">
|
||||||
|
{{ zoneName }} time zone
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div class="mr-2">
|
||||||
|
<span>Ends at</span>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
v-model="endDateInput"
|
||||||
|
placeholder="End Date"
|
||||||
|
type="date"
|
||||||
|
class="ml-2 rounded border border-slate-400 px-3 py-2"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
:disabled="!endDateInput"
|
||||||
|
placeholder="End Time"
|
||||||
|
v-model="endTimeInput"
|
||||||
|
type="time"
|
||||||
|
class="rounded border border-slate-400 ml-2 px-3 py-2"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="flex items-center mb-4"
|
class="flex items-center mt-4"
|
||||||
@click="includeLocation = !includeLocation"
|
@click="includeLocation = !includeLocation"
|
||||||
>
|
>
|
||||||
<input type="checkbox" class="mr-2" v-model="includeLocation" />
|
<input type="checkbox" class="mr-2" v-model="includeLocation" />
|
||||||
@@ -202,18 +229,10 @@
|
|||||||
import "leaflet/dist/leaflet.css";
|
import "leaflet/dist/leaflet.css";
|
||||||
import { AxiosError, AxiosRequestHeaders } from "axios";
|
import { AxiosError, AxiosRequestHeaders } from "axios";
|
||||||
import { DateTime } from "luxon";
|
import { DateTime } from "luxon";
|
||||||
import { hexToBytes } from "@noble/hashes/utils";
|
import { finalizeEvent, serializeEvent } from "nostr-tools";
|
||||||
// these core imports could also be included as "import type ..."
|
// these core imports could also be included as "import type ..."
|
||||||
import {
|
import { EventTemplate, UnsignedEvent, VerifiedEvent } from "nostr-tools/core";
|
||||||
EventTemplate,
|
import * as nip06 from "nostr-tools/nip06";
|
||||||
UnsignedEvent,
|
|
||||||
VerifiedEvent,
|
|
||||||
} from "nostr-tools/lib/types/core";
|
|
||||||
import {
|
|
||||||
accountFromExtendedKey,
|
|
||||||
extendedKeysFromSeedWords,
|
|
||||||
} from "nostr-tools/nip06";
|
|
||||||
import { finalizeEvent, serializeEvent } from "nostr-tools/pure";
|
|
||||||
import { Component, Vue } from "vue-facing-decorator";
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
import { LMap, LMarker, LTileLayer } from "@vue-leaflet/vue-leaflet";
|
import { LMap, LMarker, LTileLayer } from "@vue-leaflet/vue-leaflet";
|
||||||
import { RouteLocationNormalizedLoaded, Router } from "vue-router";
|
import { RouteLocationNormalizedLoaded, Router } from "vue-router";
|
||||||
@@ -251,6 +270,8 @@ export default class NewEditProjectView extends Vue {
|
|||||||
activeDid = "";
|
activeDid = "";
|
||||||
agentDid = "";
|
agentDid = "";
|
||||||
apiServer = "";
|
apiServer = "";
|
||||||
|
endDateInput?: string;
|
||||||
|
endTimeInput?: string;
|
||||||
errorMessage = "";
|
errorMessage = "";
|
||||||
fullClaim: PlanVerifiableCredential = {
|
fullClaim: PlanVerifiableCredential = {
|
||||||
"@context": "https://schema.org",
|
"@context": "https://schema.org",
|
||||||
@@ -325,6 +346,13 @@ export default class NewEditProjectView extends Vue {
|
|||||||
this.startDateInput = localDateTime.toFormat("yyyy-MM-dd");
|
this.startDateInput = localDateTime.toFormat("yyyy-MM-dd");
|
||||||
this.startTimeInput = localDateTime.toFormat("HH:mm");
|
this.startTimeInput = localDateTime.toFormat("HH:mm");
|
||||||
}
|
}
|
||||||
|
if (this.fullClaim.endTime) {
|
||||||
|
const localDateTime = DateTime.fromISO(
|
||||||
|
this.fullClaim.endTime as string,
|
||||||
|
).toLocal();
|
||||||
|
this.endDateInput = localDateTime.toFormat("yyyy-MM-dd");
|
||||||
|
this.endTimeInput = localDateTime.toFormat("HH:mm");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Got error retrieving that project", error);
|
console.error("Got error retrieving that project", error);
|
||||||
@@ -468,7 +496,7 @@ export default class NewEditProjectView extends Vue {
|
|||||||
group: "alert",
|
group: "alert",
|
||||||
type: "danger",
|
type: "danger",
|
||||||
title: "Date Error",
|
title: "Date Error",
|
||||||
text: "The date was invalid so it was not set.",
|
text: "The start date was invalid so it was not set.",
|
||||||
},
|
},
|
||||||
5000,
|
5000,
|
||||||
);
|
);
|
||||||
@@ -476,6 +504,28 @@ export default class NewEditProjectView extends Vue {
|
|||||||
} else {
|
} else {
|
||||||
delete vcClaim.startTime;
|
delete vcClaim.startTime;
|
||||||
}
|
}
|
||||||
|
if (this.endDateInput) {
|
||||||
|
try {
|
||||||
|
const endTimeFull = this.endTimeInput || "23:59:59";
|
||||||
|
const fullTimeString = this.endDateInput + " " + endTimeFull;
|
||||||
|
// throw an error on an invalid date or time string
|
||||||
|
vcClaim.endTime = new Date(fullTimeString).toISOString(); // ensure timezone is part of it
|
||||||
|
} catch {
|
||||||
|
// it's not a valid date so erase it and tell the user
|
||||||
|
delete vcClaim.endTime;
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Date Error",
|
||||||
|
text: "The end date was invalid so it was not set.",
|
||||||
|
},
|
||||||
|
5000,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
delete vcClaim.endTime;
|
||||||
|
}
|
||||||
const vcJwt = await createEndorserJwtVcFromClaim(this.activeDid, vcClaim);
|
const vcJwt = await createEndorserJwtVcFromClaim(this.activeDid, vcClaim);
|
||||||
|
|
||||||
// Make the xhr request payload
|
// Make the xhr request payload
|
||||||
@@ -618,15 +668,15 @@ export default class NewEditProjectView extends Vue {
|
|||||||
// remove any trailing '
|
// remove any trailing '
|
||||||
const finalDerNumNoApostrophe = finalDerNum?.replace(/'/g, "");
|
const finalDerNumNoApostrophe = finalDerNum?.replace(/'/g, "");
|
||||||
const accountNum = Number(finalDerNumNoApostrophe || 0);
|
const accountNum = Number(finalDerNumNoApostrophe || 0);
|
||||||
const extPubPri = extendedKeysFromSeedWords(
|
const extPubPri = nip06.extendedKeysFromSeedWords(
|
||||||
account?.mnemonic as string,
|
account?.mnemonic as string,
|
||||||
"",
|
"",
|
||||||
accountNum,
|
accountNum,
|
||||||
);
|
);
|
||||||
const publicExtendedKey: string = extPubPri?.publicExtendedKey;
|
const publicExtendedKey: string = extPubPri?.publicExtendedKey;
|
||||||
const privateExtendedKey = extPubPri?.privateExtendedKey;
|
const privateExtendedKey = extPubPri?.privateExtendedKey;
|
||||||
const privateKey = accountFromExtendedKey(privateExtendedKey).privateKey;
|
const privateBytes: Uint8Array =
|
||||||
const privateBytes = hexToBytes(privateKey);
|
nip06.accountFromExtendedKey(privateExtendedKey).privateKey;
|
||||||
// No real content is necessary, we just want something signed,
|
// No real content is necessary, we just want something signed,
|
||||||
// so we might as well use nostr libs for nostr functions.
|
// so we might as well use nostr libs for nostr functions.
|
||||||
// Besides: someday we may create real content that we can relay.
|
// Besides: someday we may create real content that we can relay.
|
||||||
@@ -660,7 +710,8 @@ export default class NewEditProjectView extends Vue {
|
|||||||
const endorserPartnerUrl = partnerServer + "/api/partner/link";
|
const endorserPartnerUrl = partnerServer + "/api/partner/link";
|
||||||
const timeSafariUrl = window.location.origin + "/claim/" + jwtId;
|
const timeSafariUrl = window.location.origin + "/claim/" + jwtId;
|
||||||
const content = this.fullClaim.name + " - see " + timeSafariUrl;
|
const content = this.fullClaim.name + " - see " + timeSafariUrl;
|
||||||
const publicKeyHex = accountFromExtendedKey(publicExtendedKey).publicKey;
|
const publicKeyHex =
|
||||||
|
nip06.accountFromExtendedKey(publicExtendedKey).publicKey;
|
||||||
const unsignedPayload: UnsignedEvent = {
|
const unsignedPayload: UnsignedEvent = {
|
||||||
// why doesn't "...signedPayload" work?
|
// why doesn't "...signedPayload" work?
|
||||||
kind: signedPayload.kind,
|
kind: signedPayload.kind,
|
||||||
|
|||||||
@@ -126,6 +126,7 @@ export default class OnboardMeetingListView extends Vue {
|
|||||||
attendingMeeting: Meeting | null = null;
|
attendingMeeting: Meeting | null = null;
|
||||||
firstName = "";
|
firstName = "";
|
||||||
isLoading = false;
|
isLoading = false;
|
||||||
|
isRegistered = false;
|
||||||
meetings: Meeting[] = [];
|
meetings: Meeting[] = [];
|
||||||
password = "";
|
password = "";
|
||||||
selectedMeeting: Meeting | null = null;
|
selectedMeeting: Meeting | null = null;
|
||||||
@@ -136,6 +137,7 @@ export default class OnboardMeetingListView extends Vue {
|
|||||||
this.activeDid = settings.activeDid || "";
|
this.activeDid = settings.activeDid || "";
|
||||||
this.apiServer = settings.apiServer || "";
|
this.apiServer = settings.apiServer || "";
|
||||||
this.firstName = settings.firstName || "";
|
this.firstName = settings.firstName || "";
|
||||||
|
this.isRegistered = !!settings.isRegistered;
|
||||||
await this.fetchMeetings();
|
await this.fetchMeetings();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -232,6 +234,7 @@ export default class OnboardMeetingListView extends Vue {
|
|||||||
const memberData = {
|
const memberData = {
|
||||||
name: this.firstName,
|
name: this.firstName,
|
||||||
did: this.activeDid,
|
did: this.activeDid,
|
||||||
|
isRegistered: this.isRegistered,
|
||||||
};
|
};
|
||||||
const memberDataString = JSON.stringify(memberData);
|
const memberDataString = JSON.stringify(memberData);
|
||||||
const encryptedMemberData = await encryptMessage(
|
const encryptedMemberData = await encryptMessage(
|
||||||
|
|||||||
@@ -8,8 +8,16 @@
|
|||||||
Meeting Members
|
Meeting Members
|
||||||
</h1>
|
</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 -->
|
<!-- Error State -->
|
||||||
<div v-if="errorMessage">
|
<div v-else-if="errorMessage">
|
||||||
<div class="text-center text-red-600 py-8">
|
<div class="text-center text-red-600 py-8">
|
||||||
{{ errorMessage }}
|
{{ errorMessage }}
|
||||||
</div>
|
</div>
|
||||||
@@ -19,13 +27,14 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Members List -->
|
<!-- Members List -->
|
||||||
<MembersList
|
<MembersList v-else :password="password" @error="handleError" />
|
||||||
v-else
|
|
||||||
:password="password"
|
|
||||||
:decrypt-failure-message="'That password failed. You may be in the wrong meeting. Go back and try again.'"
|
|
||||||
@error="handleError"
|
|
||||||
/>
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<UserNameDialog
|
||||||
|
ref="userNameDialog"
|
||||||
|
:callback-on-cancel="true"
|
||||||
|
sharing-explanation="This is encrypted and shared only with people in this meeting."
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
@@ -35,16 +44,35 @@ import { RouteLocation } from "vue-router";
|
|||||||
import QuickNav from "../components/QuickNav.vue";
|
import QuickNav from "../components/QuickNav.vue";
|
||||||
import TopMessage from "../components/TopMessage.vue";
|
import TopMessage from "../components/TopMessage.vue";
|
||||||
import MembersList from "../components/MembersList.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({
|
@Component({
|
||||||
components: {
|
components: {
|
||||||
QuickNav,
|
QuickNav,
|
||||||
TopMessage,
|
TopMessage,
|
||||||
MembersList,
|
MembersList,
|
||||||
|
UserNameDialog,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
export default class OnboardMeetingMembersView extends Vue {
|
export default class OnboardMeetingMembersView extends Vue {
|
||||||
|
activeDid = "";
|
||||||
|
apiServer = "";
|
||||||
errorMessage = "";
|
errorMessage = "";
|
||||||
|
firstName = "";
|
||||||
|
isRegistered = false;
|
||||||
|
isLoading = true;
|
||||||
|
|
||||||
|
$refs!: {
|
||||||
|
userNameDialog: InstanceType<typeof UserNameDialog>;
|
||||||
|
};
|
||||||
|
|
||||||
get groupId(): string {
|
get groupId(): string {
|
||||||
return (this.$route as RouteLocation).params.groupId as 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.";
|
this.errorMessage = "The password is missing. Go back and try again.";
|
||||||
return;
|
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) {
|
handleError(message: string) {
|
||||||
|
|||||||
@@ -192,23 +192,23 @@
|
|||||||
|
|
||||||
<!-- Members Section -->
|
<!-- Members Section -->
|
||||||
<div
|
<div
|
||||||
v-if="!isLoading && currentMeeting != null"
|
v-if="!isLoading && currentMeeting != null && !!currentMeeting.password"
|
||||||
class="mt-8 p-4 border rounded-lg bg-white shadow"
|
class="mt-8 p-4 border rounded-lg bg-white shadow"
|
||||||
>
|
>
|
||||||
<div class="flex items-center justify-between mb-4">
|
<div class="flex items-center justify-between mb-4">
|
||||||
<h2 class="text-2xl">Meeting Members</h2>
|
<h2 class="text-2xl">Meeting Members</h2>
|
||||||
</div>
|
</div>
|
||||||
<router-link
|
<router-link
|
||||||
|
v-if="!!currentMeeting.password"
|
||||||
:to="onboardMeetingMembersLink()"
|
:to="onboardMeetingMembersLink()"
|
||||||
class="inline-block text-blue-600"
|
class="inline-block text-blue-600"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
Open shortcut page for members <fa icon="external-link" />
|
• Open shortcut page for members <fa icon="external-link" />
|
||||||
</router-link>
|
</router-link>
|
||||||
|
|
||||||
<MembersList
|
<MembersList
|
||||||
:password="currentMeeting.password || ''"
|
:password="currentMeeting.password || ''"
|
||||||
:decrypt-failure-message="DECRYPT_FAILURE_MESSAGE"
|
|
||||||
:show-organizer-tools="true"
|
:show-organizer-tools="true"
|
||||||
@error="handleMembersError"
|
@error="handleMembersError"
|
||||||
class="mt-4"
|
class="mt-4"
|
||||||
@@ -264,15 +264,13 @@ export default class OnboardMeetingView extends Vue {
|
|||||||
timeout?: number,
|
timeout?: number,
|
||||||
) => void;
|
) => 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;
|
currentMeeting: ServerMeeting | null = null;
|
||||||
newOrUpdatedMeeting: MeetingSetupInfo | null = null;
|
newOrUpdatedMeeting: MeetingSetupInfo | null = null;
|
||||||
activeDid = "";
|
activeDid = "";
|
||||||
apiServer = "";
|
apiServer = "";
|
||||||
isDeleting = false;
|
isDeleting = false;
|
||||||
isLoading = true;
|
isLoading = true;
|
||||||
|
isRegistered = false;
|
||||||
showDeleteConfirm = false;
|
showDeleteConfirm = false;
|
||||||
fullName = "";
|
fullName = "";
|
||||||
get minDateTime() {
|
get minDateTime() {
|
||||||
@@ -286,6 +284,7 @@ export default class OnboardMeetingView extends Vue {
|
|||||||
this.activeDid = settings.activeDid || "";
|
this.activeDid = settings.activeDid || "";
|
||||||
this.apiServer = settings.apiServer || "";
|
this.apiServer = settings.apiServer || "";
|
||||||
this.fullName = settings.firstName || "";
|
this.fullName = settings.firstName || "";
|
||||||
|
this.isRegistered = !!settings.isRegistered;
|
||||||
|
|
||||||
await this.fetchCurrentMeeting();
|
await this.fetchCurrentMeeting();
|
||||||
this.isLoading = false;
|
this.isLoading = false;
|
||||||
@@ -409,6 +408,7 @@ export default class OnboardMeetingView extends Vue {
|
|||||||
const content = {
|
const content = {
|
||||||
name: this.newOrUpdatedMeeting.userFullName,
|
name: this.newOrUpdatedMeeting.userFullName,
|
||||||
did: this.activeDid,
|
did: this.activeDid,
|
||||||
|
isRegistered: this.isRegistered,
|
||||||
};
|
};
|
||||||
const encryptedContent = await encryptMessage(
|
const encryptedContent = await encryptMessage(
|
||||||
JSON.stringify(content),
|
JSON.stringify(content),
|
||||||
@@ -598,6 +598,7 @@ export default class OnboardMeetingView extends Vue {
|
|||||||
const content = {
|
const content = {
|
||||||
name: this.newOrUpdatedMeeting.userFullName,
|
name: this.newOrUpdatedMeeting.userFullName,
|
||||||
did: this.activeDid,
|
did: this.activeDid,
|
||||||
|
isRegistered: this.isRegistered,
|
||||||
};
|
};
|
||||||
const encryptedContent = await encryptMessage(
|
const encryptedContent = await encryptMessage(
|
||||||
JSON.stringify(content),
|
JSON.stringify(content),
|
||||||
|
|||||||
@@ -69,7 +69,11 @@
|
|||||||
</div>
|
</div>
|
||||||
<div v-if="startTime">
|
<div v-if="startTime">
|
||||||
<fa icon="calendar" class="fa-fw text-slate-400"></fa>
|
<fa icon="calendar" class="fa-fw text-slate-400"></fa>
|
||||||
{{ startTime }}
|
Starts {{ startTime }}
|
||||||
|
</div>
|
||||||
|
<div v-if="endTime">
|
||||||
|
<fa icon="calendar" class="fa-fw text-slate-400"></fa>
|
||||||
|
Ends {{ endTime }}
|
||||||
</div>
|
</div>
|
||||||
<div v-if="latitude || longitude">
|
<div v-if="latitude || longitude">
|
||||||
<fa icon="location-dot" class="fa-fw text-slate-400"></fa>
|
<fa icon="location-dot" class="fa-fw text-slate-400"></fa>
|
||||||
@@ -541,6 +545,7 @@ export default class ProjectViewView extends Vue {
|
|||||||
apiServer = "";
|
apiServer = "";
|
||||||
checkingConfirmationForJwtId = "";
|
checkingConfirmationForJwtId = "";
|
||||||
description = "";
|
description = "";
|
||||||
|
endTime = "";
|
||||||
expanded = false;
|
expanded = false;
|
||||||
fulfilledByThis: PlanSummaryRecord | null = null;
|
fulfilledByThis: PlanSummaryRecord | null = null;
|
||||||
fulfillersToThis: Array<PlanSummaryRecord> = [];
|
fulfillersToThis: Array<PlanSummaryRecord> = [];
|
||||||
@@ -641,6 +646,14 @@ export default class ProjectViewView extends Vue {
|
|||||||
" " +
|
" " +
|
||||||
startDateTime.toLocaleTimeString();
|
startDateTime.toLocaleTimeString();
|
||||||
}
|
}
|
||||||
|
const endTime = resp.data.claim?.endTime;
|
||||||
|
if (endTime != null) {
|
||||||
|
const endDateTime = new Date(endTime);
|
||||||
|
this.endTime =
|
||||||
|
endDateTime.toLocaleDateString() +
|
||||||
|
" " +
|
||||||
|
endDateTime.toLocaleTimeString();
|
||||||
|
}
|
||||||
this.agentDid = resp.data.claim?.agent?.identifier;
|
this.agentDid = resp.data.claim?.agent?.identifier;
|
||||||
this.agentDidVisibleToDids =
|
this.agentDidVisibleToDids =
|
||||||
resp.data.claim?.agent?.identifierVisibleToDids || [];
|
resp.data.claim?.agent?.identifierVisibleToDids || [];
|
||||||
|
|||||||
Reference in New Issue
Block a user