summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorJohn Molakvoæ (skjnldsv) <skjnldsv@protonmail.com>2021-04-27 19:38:41 +0200
committerJohn Molakvoæ (skjnldsv) <skjnldsv@protonmail.com>2021-05-30 10:28:57 +0200
commit9facd8fbf3621df573f0e56e54da0612482c3c62 (patch)
treef0130a5354a36f312444e017731bb2951f10d17a /src
parentbb5f38e9231b659f348fbd83422af0d65194037b (diff)
Refactor circle actions
Signed-off-by: John Molakvoæ (skjnldsv) <skjnldsv@protonmail.com>
Diffstat (limited to 'src')
-rw-r--r--src/components/AppContent/CircleContent.vue48
-rw-r--r--src/components/AppNavigation/CircleNavigationItem.vue93
-rw-r--r--src/components/AppNavigation/RootNavigation.vue10
-rw-r--r--src/components/CircleDetails.vue134
-rw-r--r--src/components/CircleDetails/CircleConfigs.vue35
-rw-r--r--src/components/CircleDetails/ContentHeading.vue13
-rw-r--r--src/components/ContactDetails.vue1
-rw-r--r--src/components/DetailsHeader.vue10
-rw-r--r--src/components/MembersList/MembersListItem.vue13
-rw-r--r--src/mixins/CircleActionsMixin.js152
-rw-r--r--src/models/constants.ts6
-rw-r--r--src/services/circles.d.ts14
-rw-r--r--src/services/circles.ts18
-rw-r--r--src/store/circles.js24
14 files changed, 405 insertions, 166 deletions
diff --git a/src/components/AppContent/CircleContent.vue b/src/components/AppContent/CircleContent.vue
index 164aaf5b..eda997df 100644
--- a/src/components/AppContent/CircleContent.vue
+++ b/src/components/AppContent/CircleContent.vue
@@ -36,34 +36,36 @@
</EmptyContent>
</AppContentDetails>
- <!-- not a member -->
- <AppContentDetails v-else-if="!circle.isMember">
- <EmptyContent v-if="!loadingJoin" icon="icon-circles">
- {{ t('contacts', 'You are not a member of this circle') }}
-
- <!-- Only show the join button if the circle is accepting requests -->
- <template v-if="circle.canJoin" #desc>
- <button :disabled="loadingJoin" class="primary" @click="requestJoin">
- {{ t('contacts', 'Request to join') }}
- </button>
- </template>
- </EmptyContent>
-
- <EmptyContent v-else-if="circle.isPendingJoin" icon="icon-loading">
- {{ t('contacts', 'Your request to join this circle is pending approval') }}
- </EmptyContent>
-
- <EmptyContent v-else icon="icon-loading">
- {{ t('contacts', 'Joining circle') }}
- </EmptyContent>
- </AppContentDetails>
-
<template v-else>
<!-- member list -->
<MemberList :list="members" />
<!-- main contacts details -->
- <CircleDetails :circle-id="selectedCircle" />
+ <CircleDetails :circle="circle">
+ <!-- not a member -->
+ <template v-if="!circle.isMember">
+ <!-- Join request in progress -->
+ <EmptyContent v-if="loadingJoin" icon="icon-loading">
+ {{ t('contacts', 'Joining circle') }}
+ </EmptyContent>
+
+ <!-- Pending request validation -->
+ <EmptyContent v-else-if="circle.isPendingJoin" icon="icon-loading">
+ {{ t('contacts', 'Your request to join this circle is pending approval') }}
+ </EmptyContent>
+
+ <EmptyContent v-else icon="icon-circles">
+ {{ t('contacts', 'You are not a member of this circle') }}
+
+ <!-- Only show the join button if the circle is accepting requests -->
+ <template v-if="circle.canJoin" #desc>
+ <button :disabled="loadingJoin" class="primary" @click="requestJoin">
+ {{ t('contacts', 'Request to join') }}
+ </button>
+ </template>
+ </EmptyContent>
+ </template>
+ </CircleDetails>
</template>
</div>
</AppContent>
diff --git a/src/components/AppNavigation/CircleNavigationItem.vue b/src/components/AppNavigation/CircleNavigationItem.vue
index eadea9a7..d6787a6e 100644
--- a/src/components/AppNavigation/CircleNavigationItem.vue
+++ b/src/components/AppNavigation/CircleNavigationItem.vue
@@ -1,5 +1,5 @@
<!--
- - @copyright Copyright (c) 2018 John Molakvoæ <skjnldsv@protonmail.com>
+ - @copyright Copyright (c) 2021 John Molakvoæ <skjnldsv@protonmail.com>
-
- @author John Molakvoæ <skjnldsv@protonmail.com>
-
@@ -25,7 +25,7 @@
:to="circle.router"
:title="circle.displayName"
:icon="circle.icon">
- <template v-if="loading" slot="actions">
+ <template v-if="loadingAction" slot="actions">
<ActionText icon="icon-loading-small">
{{ t('contacts', 'Loading …') }}
</ActionText>
@@ -50,7 +50,7 @@
<!-- leave circle -->
<ActionButton
v-if="circle.canLeave"
- @click="leaveCircle">
+ @click="confirmLeaveCircle">
{{ t('contacts', 'Leave circle') }}
<ExitToApp slot="icon"
:size="16"
@@ -71,7 +71,7 @@
<ActionButton
v-if="circle.canDelete"
icon="icon-delete"
- @click="deleteCircle">
+ @click="confirmDeleteCircle">
{{ t('contacts', 'Delete') }}
</ActionButton>
</template>
@@ -83,8 +83,6 @@
</template>
<script>
-import { emit } from '@nextcloud/event-bus'
-
import ActionButton from '@nextcloud/vue/dist/Components/ActionButton'
import ActionLink from '@nextcloud/vue/dist/Components/ActionLink'
import ActionText from '@nextcloud/vue/dist/Components/ActionText'
@@ -93,10 +91,8 @@ import AppNavigationItem from '@nextcloud/vue/dist/Components/AppNavigationItem'
import ExitToApp from 'vue-material-design-icons/ExitToApp'
import LocationEnter from 'vue-material-design-icons/LocationEnter'
-import { joinCircle } from '../../services/circles.ts'
-import { showError } from '@nextcloud/dialogs'
import Circle from '../../models/circle.ts'
-import CopyToClipboardMixin from '../../mixins/CopyToClipboardMixin'
+import CircleActionsMixin from '../../mixins/CircleActionsMixin'
export default {
name: 'CircleNavigationItem',
@@ -111,7 +107,7 @@ export default {
LocationEnter,
},
- mixins: [CopyToClipboardMixin],
+ mixins: [CircleActionsMixin],
props: {
circle: {
@@ -120,87 +116,10 @@ export default {
},
},
- data() {
- return {
- loading: false,
- }
- },
-
computed: {
- copyButtonText() {
- if (this.copied) {
- return this.copySuccess
- ? t('contacts', 'Copied')
- : t('contacts', 'Could not copy')
- }
- return t('contacts', 'Copy link')
- },
-
- circleUrl() {
- const route = this.$router.resolve(this.circle.router)
- return window.location.origin + route.href
- },
-
- joinButtonTitle() {
- if (this.circle.requireJoinAccept) {
- return t('contacts', 'Request to join')
- }
- return t('contacts', 'Join circle')
- },
-
memberCount() {
return Object.values(this.circle?.members || []).length
},
},
-
- methods: {
- // Trigger the entity picker view
- async addMemberToCircle() {
- await this.$router.push(this.circle.router)
- emit('contacts:circles:append', this.circle.id)
- },
-
- async joinCircle() {
- this.loading = true
- try {
- await joinCircle(this.circle.id)
- } catch (error) {
- showError(t('contacts', 'Unable to join the circle'))
- } finally {
- this.loading = false
- }
-
- },
-
- async leaveCircle() {
- this.loading = true
- const member = this.circle.initiator
-
- try {
- await this.$store.dispatch('deleteMemberFromCircle', {
- member,
- leave: true,
- })
- } catch (error) {
- console.error('Could not leave the circle', member, error)
- showError(t('contacts', 'Could not leave the circle {displayName}', this.circle))
- } finally {
- this.loading = false
- }
-
- },
-
- async deleteCircle() {
- this.loading = true
-
- try {
- this.$store.dispatch('deleteCircle', this.circle.id)
- } catch (error) {
- showError(t('contacts', 'Unable to delete the circle'))
- } finally {
- this.loading = false
- }
- },
- },
}
</script>
diff --git a/src/components/AppNavigation/RootNavigation.vue b/src/components/AppNavigation/RootNavigation.vue
index 675f5750..38398fab 100644
--- a/src/components/AppNavigation/RootNavigation.vue
+++ b/src/components/AppNavigation/RootNavigation.vue
@@ -69,7 +69,7 @@
</AppNavigationCounter>
</AppNavigationItem>
- <AppNavigationItem
+ <AppNavigationCaption
id="newgroup"
:force-menu="true"
:menu-open.sync="isNewGroupMenuOpen"
@@ -85,7 +85,7 @@
:placeholder="t('contacts','Group name')"
@submit.prevent.stop="createNewGroup" />
</template>
- </AppNavigationItem>
+ </AppNavigationCaption>
<!-- Custom groups -->
<GroupNavigationItem
@@ -101,7 +101,7 @@
icon=""
@click="onToggleGroups" />
- <AppNavigationItem
+ <AppNavigationCaption
id="newcircle"
:force-menu="true"
:menu-open.sync="isNewCircleMenuOpen"
@@ -117,7 +117,7 @@
:placeholder="t('contacts','Circle name')"
@submit.prevent.stop="createNewCircle" />
</template>
- </AppNavigationItem>
+ </AppNavigationCaption>
<!-- Circles -->
<CircleNavigationItem
@@ -152,6 +152,7 @@ import AppNavigation from '@nextcloud/vue/dist/Components/AppNavigation'
import AppNavigationCounter from '@nextcloud/vue/dist/Components/AppNavigationCounter'
import AppNavigationItem from '@nextcloud/vue/dist/Components/AppNavigationItem'
import AppNavigationSettings from '@nextcloud/vue/dist/Components/AppNavigationSettings'
+import AppNavigationCaption from '@nextcloud/vue/dist/Components/AppNavigationCaption'
import naturalCompare from 'string-natural-compare'
@@ -171,6 +172,7 @@ export default {
AppNavigationCounter,
AppNavigationItem,
AppNavigationSettings,
+ AppNavigationCaption,
CircleNavigationItem,
GroupNavigationItem,
SettingsSection,
diff --git a/src/components/CircleDetails.vue b/src/components/CircleDetails.vue
index 98e4870c..71a4e5dd 100644
--- a/src/components/CircleDetails.vue
+++ b/src/components/CircleDetails.vue
@@ -44,44 +44,101 @@
autocorrect="off"
spellcheck="false"
name="displayname"
- @input="debounceUpdateCircle">
+ @input="onDisplayNameChangeDebounce">
<!-- org, title -->
- <template #subtitle>
+ <template v-if="!circle.isOwner" #subtitle>
+ {{ t('contacts', 'Circle owned by {owner}', { owner: circle.owner.displayName}) }}
</template>
<!-- actions -->
<template #actions>
+ <Actions>
+ <!-- leave circle -->
+ <ActionButton
+ v-if="circle.canLeave"
+ @click="confirmLeaveCircle">
+ {{ t('contacts', 'Leave circle') }}
+ <ExitToApp slot="icon"
+ :size="16"
+ decorative />
+ </ActionButton>
+
+ <!-- join circle -->
+ <ActionButton
+ v-else-if="!circle.isMember && circle.canJoin"
+ @click="joinCircle">
+ {{ joinButtonTitle }}
+ <LocationEnter slot="icon"
+ :size="16"
+ decorative />
+ </ActionButton>
+ </Actions>
+ <Actions>
+ <!-- copy circle link -->
+ <ActionLink
+ :href="circleUrl"
+ :icon="copyLoading ? 'icon-loading-small' : 'icon-public'"
+ @click.stop.prevent="copyToClipboard(circleUrl)">
+ {{ copyButtonText }}
+ </ActionLink>
+ </Actions>
</template>
<!-- menu actions -->
<template #actions-menu>
+ <!-- delete circle -->
+ <ActionButton
+ v-if="circle.canDelete"
+ icon="icon-delete"
+ @click="confirmDeleteCircle">
+ {{ t('contacts', 'Delete') }}
+ </ActionButton>
</template>
</DetailsHeader>
<section class="circle-details-section">
- <ContentHeading>{{ t('contacts', 'Description') }}</ContentHeading>
+ <ContentHeading :loading="loadingDescription">
+ {{ t('contacts', 'Description') }}
+ </ContentHeading>
- <RichContenteditable class="circle-details-section__description"
- :value="circle.description"
+ <RichContenteditable
+ :value.sync="circle.description"
:auto-complete="onAutocomplete"
:maxlength="1024"
:multiline="true"
- :disabled="loading"
- :placeholder="t('contacts', 'Enter a description for the circle')"
- @submit="onDescriptionSubmit" />
+ :contenteditable="circle.isOwner"
+ :placeholder="descriptionPlaceholder"
+ class="circle-details-section__description"
+ @update:value="onDescriptionChangeDebounce" />
</section>
- <section class="circle-details-section">
+ <section v-if="circle.isOwner" class="circle-details-section">
<CircleConfigs class="circle-details-section__configs" :circle="circle" />
</section>
+
+ <section v-else>
+ <slot />
+ </section>
</AppContentDetails>
</template>
<script>
+import { showError } from '@nextcloud/dialogs'
+import debounce from 'debounce'
+
+import Actions from '@nextcloud/vue/dist/Components/Actions'
+import ActionButton from '@nextcloud/vue/dist/Components/ActionButton'
+import ActionLink from '@nextcloud/vue/dist/Components/ActionLink'
import AppContentDetails from '@nextcloud/vue/dist/Components/AppContentDetails'
import Avatar from '@nextcloud/vue/dist/Components/Avatar'
import RichContenteditable from '@nextcloud/vue/dist/Components/RichContenteditable'
+
+import ExitToApp from 'vue-material-design-icons/ExitToApp'
+import LocationEnter from 'vue-material-design-icons/LocationEnter'
+
+import { CircleEdit, editCircle } from '../services/circles.ts'
+import CircleActionsMixin from '../mixins/CircleActionsMixin'
import DetailsHeader from './DetailsHeader'
import CircleConfigs from './CircleDetails/CircleConfigs'
import ContentHeading from './CircleDetails/ContentHeading'
@@ -90,26 +147,36 @@ export default {
name: 'CircleDetails',
components: {
+ ActionButton,
+ ActionLink,
+ Actions,
AppContentDetails,
Avatar,
CircleConfigs,
ContentHeading,
DetailsHeader,
+ ExitToApp,
+ LocationEnter,
RichContenteditable,
},
- props: {
- circleId: {
- type: String,
- required: true,
- },
+ mixins: [CircleActionsMixin],
+
+ data() {
+ return {
+ loadingDescription: false,
+ }
},
computed: {
- circle() {
- return this.$store.getters.getCircle(this.circleId)
+ descriptionPlaceholder() {
+ if (this.circle.description.trim() === '') {
+ return t('contacts', 'There is no description for this circle')
+ }
+ return t('contacts', 'Enter a description for the circle')
},
},
+
methods: {
/**
* Autocomplete @mentions on the description
@@ -122,8 +189,34 @@ export default {
callback([])
},
- onDescriptionSubmit() {
- console.info(...arguments)
+ onDescriptionChangeDebounce: debounce(function() {
+ this.onDescriptionChange(...arguments)
+ }, 500),
+ async onDescriptionChange(description) {
+ this.loadingDescription = true
+ try {
+ await editCircle(this.circle.id, CircleEdit.Description, description)
+ } catch (error) {
+ console.error('Unable to edit circle description', description, error)
+ showError(t('contacts', 'An error happened during description sync'))
+ } finally {
+ this.loadingDescription = false
+ }
+ },
+
+ onDisplayNameChangeDebounce: debounce(function() {
+ this.onDisplayNameChange(...arguments)
+ }, 500),
+ async onDisplayNameChange(description) {
+ this.loadingDescription = true
+ try {
+ await editCircle(this.circle.id, CircleEdit.Description, description)
+ } catch (error) {
+ console.error('Unable to edit circle description', description, error)
+ showError(t('contacts', 'An error happened during description sync'))
+ } finally {
+ this.loadingDescription = false
+ }
},
},
}
@@ -133,17 +226,16 @@ export default {
.app-content-details {
flex: 1 1 100%;
min-width: 0;
+ padding: 0 80px;
}
.circle-details-section {
- padding: 0 80px;
-
&:not(:first-of-type) {
margin-top: 24px;
}
&__description {
- max-width: 400px;
+ max-width: 800px;
}
}
</style>
diff --git a/src/components/CircleDetails/CircleConfigs.vue b/src/components/CircleDetails/CircleConfigs.vue
index bd7b27a8..2db38ab4 100644
--- a/src/components/CircleDetails/CircleConfigs.vue
+++ b/src/components/CircleDetails/CircleConfigs.vue
@@ -28,31 +28,34 @@
</ContentHeading>
<ul class="circle-config__list">
- <CheckboxRadio v-for="(label, config) in configs"
+ <CheckboxRadioSwitch v-for="(label, config) in configs"
:key="'circle-config' + config"
:checked="isChecked(config)"
+ :loading="loading === config"
+ :disabled="loading !== false"
wrapper-element="li"
@update:checked="onChange(config, $event)">
{{ label }}
- </CheckboxRadio>
+ </CheckboxRadioSwitch>
</ul>
</li>
</ul>
</template>
<script>
-import CheckboxRadio from '@nextcloud/vue/dist/Components/CheckboxRadio'
+import CheckboxRadioSwitch from '@nextcloud/vue/dist/Components/CheckboxRadioSwitch'
import ContentHeading from './ContentHeading'
import { PUBLIC_CIRCLE_CONFIG } from '../../models/constants.ts'
import Circle from '../../models/circle.ts'
-import { CircleEdit, editCircle } from '../../services/circles'
+import { CircleEdit, editCircle } from '../../services/circles.ts'
+import { showError } from '@nextcloud/dialogs'
export default {
name: 'CircleConfigs',
components: {
- CheckboxRadio,
+ CheckboxRadioSwitch,
ContentHeading,
},
@@ -66,6 +69,8 @@ export default {
data() {
return {
PUBLIC_CIRCLE_CONFIG,
+
+ loading: false,
}
},
@@ -80,21 +85,29 @@ export default {
* @param {boolean} checked checked or not
*/
async onChange(config, checked) {
- console.debug('Circle config', `'${PUBLIC_CIRCLE_CONFIG[config]}'`, 'is set to', checked)
+ console.debug('Circle config', config, 'is set to', checked)
+ this.loading = config
const prevConfig = this.circle.config
-
if (checked) {
// eslint-disable-next-line vue/no-mutating-props
- this.circle.config = prevConfig | config
+ config = prevConfig | config
} else {
// eslint-disable-next-line vue/no-mutating-props
- this.circle.config = prevConfig & ~config
+ config = prevConfig & ~config
}
- const data = await editCircle(this.circle.id, CircleEdit.Config, this.circle.config)
- console.info(data)
+ try {
+ const circleData = await editCircle(this.circle.id, CircleEdit.Config, config)
+ // eslint-disable-next-line vue/no-mutating-props
+ this.circle.config = circleData.config
+ } catch (error) {
+ console.error('Unable to edit circle config', prevConfig, config, error)
+ showError(t('contacts', 'An error happened during the config change'))
+ } finally {
+ this.loading = false
+ }
},
},
}
diff --git a/src/components/CircleDetails/ContentHeading.vue b/src/components/CircleDetails/ContentHeading.vue
index 2f435a0f..4cc03e5f 100644
--- a/src/components/CircleDetails/ContentHeading.vue
+++ b/src/components/CircleDetails/ContentHeading.vue
@@ -23,17 +23,30 @@
<template>
<h3 class="app-content-heading">
<slot />
+ <div v-if="loading" class="app-content-heading__loader icon-loading-small" />
</h3>
</template>
<script>
export default {
name: 'ContentHeading',
+
+ props: {
+ loading: {
+ type: Boolean,
+ default: false,
+ },
+ },
}
</script>
<style lang="scss" scoped>
.app-content-heading {
font-weight: bold;
+ display: flex;
+
+ &__loader {
+ margin-left: 8px;
+ }
}
</style>
diff --git a/src/components/ContactDetails.vue b/src/components/ContactDetails.vue
index f4e53ab4..5c316633 100644
--- a/src/components/ContactDetails.vue
+++ b/src/components/ContactDetails.vue
@@ -771,6 +771,7 @@ export default {
.app-content-details {
flex: 1 1 100%;
min-width: 0;
+ padding: 0 80px;
}
// List of all properties
diff --git a/src/components/DetailsHeader.vue b/src/components/DetailsHeader.vue
index 6723bf10..b2b4d1a2 100644
--- a/src/components/DetailsHeader.vue
+++ b/src/components/DetailsHeader.vue
@@ -32,7 +32,7 @@
<h2 class="contact-header__infos-title">
<slot name="title" />
</h2>
- <div class="contact-header__infos-subtitle">
+ <div v-if="$slots.subtitle" class="contact-header__infos-subtitle">
<slot name="subtitle" />
</div>
</div>
@@ -84,14 +84,12 @@ export default {
display: flex;
align-items: center;
padding: 50px 0 20px;
- font-weight: bold;
&__avatar {
position: relative;
- flex: 1 1 var(--avatar-size);
- min-width: var(--avatar-size);
- max-width: 120px;
+ flex: 0 0 var(--avatar-size);
margin: 10px;
+ margin-left: 0;
display: flex;
justify-content: flex-end;
}
@@ -114,7 +112,7 @@ export default {
min-width: 100px;
max-width: 100%;
margin: 0;
- padding: 4px 5px;
+ padding: 0;
white-space: nowrap;
text-overflow: ellipsis;
border: none;
diff --git a/src/components/MembersList/MembersListItem.vue b/src/components/MembersList/MembersListItem.vue
index ae50e85c..9eb5a120 100644
--- a/src/components/MembersList/MembersListItem.vue
+++ b/src/components/MembersList/MembersListItem.vue
@@ -165,7 +165,7 @@ export default {
// Object.keys returns those as string
.map(level => parseInt(level, 10))
// we cannot set to a level higher than the current user's level
- .filter(level => level < this.currentUserLevel)
+ .filter(level => level <= this.currentUserLevel)
// we cannot set to the level this member is already
.filter(level => level !== this.source.level)
},
@@ -209,6 +209,10 @@ export default {
* @returns {string}
*/
levelChangeLabel(level) {
+ if (level === MemberLevels.OWNER) {
+ return t('contacts', 'Promote as sole owner')
+ }
+
if (this.source.level < level) {
return t('contacts', 'Promote to {level}', { level: CIRCLES_MEMBER_LEVELS[level] })
}
@@ -245,6 +249,13 @@ export default {
await changeMemberLevel(this.circle.id, this.source.id, level)
this.showLevelMenu = false
+ // If we changed an owner, let's refresh the whole dataset to update all ownership & memberships
+ if (level === MemberLevels.OWNER) {
+ await this.$store.dispatch('getCircle', this.circle.id)
+ await this.$store.dispatch('getCircleMembers', this.circle.id)
+ return
+ }
+
// this.source is a class. We're modifying the class setter, not the prop itself
// eslint-disable-next-line vue/no-mutating-props
this.source.level = level
diff --git a/src/mixins/CircleActionsMixin.js b/src/mixins/CircleActionsMixin.js
new file mode 100644
index 00000000..a11cc1c0
--- /dev/null
+++ b/src/mixins/CircleActionsMixin.js
@@ -0,0 +1,152 @@
+/**
+ * @copyright Copyright (c) 2021 John Molakvoæ <skjnldsv@protonmail.com>
+ *
+ * @author John Molakvoæ <skjnldsv@protonmail.com>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+import { emit } from '@nextcloud/event-bus'
+import { showError } from '@nextcloud/dialogs'
+
+import { joinCircle } from '../services/circles.ts'
+import Circle from '../models/circle.ts'
+import CopyToClipboardMixin from './CopyToClipboardMixin'
+
+export default {
+
+ props: {
+ circle: {
+ type: Circle,
+ required: true,
+ },
+ },
+
+ mixins: [CopyToClipboardMixin],
+
+ data() {
+ return {
+ loadingAction: false,
+ }
+ },
+
+ computed: {
+ copyButtonText() {
+ if (this.copied) {
+ return this.copySuccess
+ ? t('contacts', 'Copied')
+ : t('contacts', 'Could not copy')
+ }
+ return t('contacts', 'Copy link')
+ },
+
+ circleUrl() {
+ const route = this.$router.resolve(this.circle.router)
+ return window.location.origin + route.href
+ },
+
+ joinButtonTitle() {
+ if (this.circle.requireJoinAccept) {
+ return t('contacts', 'Request to join')
+ }
+ return t('contacts', 'Join circle')
+ },
+ },
+
+ methods: {
+ confirmLeaveCircle() {
+ OC.dialogs.confirmDestructive(
+ t('contacts', 'You are about to leave {circle}.\n Are you sure ?', {
+ circle: this.circle.displayName,
+ }),
+ t('contacts', 'Please confirm circle leave'),
+ OC.dialogs.YES_NO_BUTTONS,
+ this.leaveCircle,
+ true
+ )
+ },
+ async leaveCircle(confirm) {
+ if (!confirm) {
+ console.debug('Circle leave cancelled')
+ return
+ }
+
+ this.loadingAction = true
+ const member = this.circle.initiator
+
+ try {
+ await this.$store.dispatch('deleteMemberFromCircle', {
+ member,
+ leave: true,
+ })
+ } catch (error) {
+ console.error('Could not leave the circle', member, error)
+ showError(t('contacts', 'Could not leave the circle {displayName}', this.circle))
+ } finally {
+ this.loadingAction = false
+ }
+
+ },
+
+ async joinCircle() {
+ this.loadingAction = true
+ try {
+ await joinCircle(this.circle.id)
+ } catch (error) {
+ showError(t('contacts', 'Unable to join the circle'))
+ } finally {
+ this.loadingAction = false
+ }
+
+ },
+
+ confirmDeleteCircle() {
+ OC.dialogs.confirmDestructive(
+ t('contacts', 'You are about to delete {circle}.\n Are you sure ?', {
+ circle: this.cir