Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions lib/Controller/PageController.php
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,10 @@ public function index(): TemplateResponse {
$isCircleVersionCompatible = $this->compareVersion->isCompatible($circleVersion ? $circleVersion : '0.0.0', 22);
// Check whether group sharing is enabled or not
$isGroupSharingEnabled = $this->config->getAppValue('core', 'shareapi_allow_group_sharing', 'yes') === 'yes';
$talkVersion = $this->appManager->getAppVersion('spreed');
$isTalkEnabled = $this->appManager->isEnabledForUser('spreed') === true;

$isTalkVersionCompatible = $this->compareVersion->isCompatible($talkVersion ? $talkVersion : '0.0.0', 2);

$this->initialStateService->provideInitialState(Application::APP_ID, 'isGroupSharingEnabled', $isGroupSharingEnabled);
$this->initialStateService->provideInitialState(Application::APP_ID, 'locales', $locales);
Expand All @@ -117,6 +121,7 @@ public function index(): TemplateResponse {
$this->initialStateService->provideInitialState(Application::APP_ID, 'enableSocialSync', $bgSyncEnabledByUser);
$this->initialStateService->provideInitialState(Application::APP_ID, 'isContactsInteractionEnabled', $isContactsInteractionEnabled);
$this->initialStateService->provideInitialState(Application::APP_ID, 'isCirclesEnabled', $isCirclesEnabled && $isCircleVersionCompatible);
$this->initialStateService->provideInitialState(Application::APP_ID, 'isTalkEnabled', $isTalkEnabled && $isTalkVersionCompatible);

Util::addScript(Application::APP_ID, 'contacts-main');

Expand Down
180 changes: 145 additions & 35 deletions src/components/ContactDetails.vue
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@
</EmptyContent>

<!-- TODO: add empty content while this.loadingData === true -->

<template v-else>
<!-- contact header -->
<DetailsHeader>
Expand Down Expand Up @@ -141,7 +140,6 @@
</NcButton>
</template>
</template>

<!-- menu actions -->
<template #actions-menu>
<ActionLink :href="contact.url"
Expand Down Expand Up @@ -221,32 +219,69 @@

<!-- contact details loading -->
<IconLoading v-if="loadingData" :size="20" class="contact-details" />

<!-- contact details -->
<section v-else class="contact-details">
<!-- properties iteration -->
<!-- using contact.key in the key and index as key to avoid conflicts between similar data and exact key -->

<div v-for="(properties, name) in groupedProperties"
:key="name">
<ContactDetailsProperty v-for="(property, index) in properties"
:key="`${index}-${contact.key}-${property.name}`"
:is-first-property="index===0"
:is-last-property="index === properties.length - 1"
:property="property"
:contact="contact"
:local-contact="localContact"
:contacts="contacts"
:bus="bus"
:is-read-only="isReadOnly" />
<!-- quick actions -->
<div v-else-if="!loadingData" class="contact-details-wrapper">
<div v-if="!editMode" class="quick-actions">
<Actions v-if="emailAddressProperties">
<template #icon>
<IconMail :size="20" />
</template>
<ActionLink v-for="emailAddress in emailAddressList"
:key="emailAddress"
:href="'mailto:' + emailAddress">
<template #icon>
<IconMail :size="20" />
</template>
{{ emailAddress }}
</ActionLink>
</Actions>
<Actions v-if="phoneNumberProperties">
<template #icon>
<IconCall :size="20" />
</template>
<ActionLink v-for="phoneNumber in phoneNumberList"
:key="phoneNumber"
:href="'tel:' + phoneNumber">
<template #icon>
<IconCall :size="20" />
</template>
{{ phoneNumber }}
</ActionLink>
</Actions>
<NcButton v-if="isTalkEnabled && isInSystemAddressBook"
class="icon-talk"
:href="callUrl" />
<NcButton v-if="profilePageLink"
:href="profilePageLink">
<template #icon>
<IconAccount :size="20" />
</template>
</NcButton>
</div>
<!-- contact details -->
<section class="contact-details">
<!-- properties iteration -->
<!-- using contact.key in the key and index as key to avoid conflicts between similar data and exact key -->

<div v-for="(properties, name) in groupedProperties"
:key="name">
<ContactDetailsProperty v-for="(property, index) in properties"
:key="`${index}-${contact.key}-${property.name}`"
:is-first-property="index===0"
:is-last-property="index === properties.length - 1"
:property="property"
:contact="contact"
:local-contact="localContact"
:contacts="contacts"
:bus="bus"
:is-read-only="isReadOnly" />
</div>
</section>

<!-- addressbook change select - no last property because class is not applied here,
empty property because this is a required prop on regular property-select. But since
we are hijacking this... (this is supposed to be used with a ICAL.property, but to avoid code
duplication, we created a fake propModel and property with our own options here) -->
<!-- We need to pass all addressbooksOptions not only writable ones. Otherwise the the name
can't be displayed for readOnly addressbooks -->
<PropertySelect :prop-model="addressbookModel"
:options="addressbooksOptions"
:value.sync="addressbook"
Expand All @@ -259,30 +294,30 @@

<!-- Groups always visible -->
<PropertyGroups :prop-model="groupsModel"
:value="localContact.groups"
:value.sync="localContact.groups"
:contact="contact"
:is-read-only="isReadOnly"
class="property--groups property--last"
@update:value="updateGroups" />

<!-- new property select -->
<AddNewProp v-if="!isReadOnly"
:bus="bus"
:contact="contact" />

<!-- Last modified-->
<PropertyRev v-if="contact.rev" :value="contact.rev" />
</section>
class="property--groups property--last" />
</div>
<!-- new property select -->
<AddNewProp v-if="!isReadOnly"
:bus="bus"
:contact="contact" />

<!-- Last modified-->
<PropertyRev v-if="contact.rev" :value="contact.rev" />
</template>
</AppContentDetails>
</template>

<script>
import { showError } from '@nextcloud/dialogs'

import { stringify } from 'ical.js'
import qr from 'qr-image'
import Vue from 'vue'
import {
NcActions as Actions,
NcActionButton as ActionButton,
NcActionLink as ActionLink,
NcAppContentDetails as AppContentDetails,
Expand All @@ -298,6 +333,10 @@ import IconDownload from 'vue-material-design-icons/Download.vue'
import IconDelete from 'vue-material-design-icons/Delete.vue'
import IconQr from 'vue-material-design-icons/Qrcode.vue'
import CakeIcon from 'vue-material-design-icons/Cake.vue'
import IconMail from 'vue-material-design-icons/Email.vue'
import IconCall from 'vue-material-design-icons/Phone.vue'
import IconMessage from 'vue-material-design-icons/MessageProcessing.vue'
import IconAccount from 'vue-material-design-icons/Account.vue'
import IconCopy from 'vue-material-design-icons/ContentCopy.vue'
import PencilIcon from 'vue-material-design-icons/Pencil.vue'
import CheckIcon from 'vue-material-design-icons/Check.vue'
Expand All @@ -313,11 +352,17 @@ import DetailsHeader from './DetailsHeader.vue'
import PropertyGroups from './Properties/PropertyGroups.vue'
import PropertyRev from './Properties/PropertyRev.vue'
import PropertySelect from './Properties/PropertySelect.vue'
import { generateUrl } from '@nextcloud/router'
import { loadState } from '@nextcloud/initial-state'
import isTalkEnabled from '../services/isTalkEnabled.js'

const { profileEnabled } = loadState('user_status', 'profileEnabled', false)

export default {
name: 'ContactDetails',

components: {
Actions,
ActionButton,
ActionLink,
AddNewProp,
Expand All @@ -327,6 +372,10 @@ export default {
DetailsHeader,
EmptyContent,
IconContact,
IconMail,
IconMessage,
IconCall,
IconAccount,
IconDownload,
IconDelete,
IconQr,
Expand Down Expand Up @@ -384,6 +433,9 @@ export default {

// communication for ContactDetailsAddNewProp and ContactDetailsProperty
bus: new Vue(),
showMenuPopover: false,
profileEnabled,
isTalkEnabled,
}
},

Expand Down Expand Up @@ -592,7 +644,30 @@ export default {

return ''
},

profilePageLink() {
return this.contact.socialLink('NEXTCLOUD')
},
emailAddressProperties() {
return this.localContact.properties.find(property => property.name === 'email')
},
emailAddress() {
return this.emailAddressProperties?.getFirstValue()
},
phoneNumberProperties() {
return this.localContact.properties.find(property => property.name === 'tel')
},
phoneNumberList() {
return this.groupedProperties?.tel?.map(prop => prop.getFirstValue())
},
emailAddressList() {
return this.groupedProperties?.email?.map(prop => prop.getFirstValue())
},
callUrl() {
return generateUrl('/apps/spreed/?callUser={uid}', { uid: this.contact.uid })
},
isInSystemAddressBook() {
return this.contact.addressbook.id === 'z-server-generated--system'
},
},

watch: {
Expand Down Expand Up @@ -899,12 +974,37 @@ export default {

<style lang="scss" scoped>
// List of all properties
.contact-details-wrapper {
display: inline;
align-items: flex-start;
padding: 50px 0 20px;
gap: 15px;
}
@media only screen and (max-width: 600px) {
.contact-details-wrapper {
display: block;
}
}
section.contact-details {
display: flex;
flex-direction: column;
gap: 40px;
}
.quick-actions {
display: flex;
flex: 1 0 auto;
gap: 15px;
float: right;
margin-right: 100px;
margin-top: 40px;

}
@media only screen and (max-width: 600px) {
.quick-actions {
float: left;
margin-top: -44px;
}
}
#qrcode-modal {
::v-deep .modal-container {
display: flex;
Expand Down Expand Up @@ -941,4 +1041,14 @@ section.contact-details {
}
}
}
.action-item {
background-color: var(--color-primary-element-light);
border-radius: var(--border-radius-rounded);
}
::v-deep .button-vue--vue-tertiary:hover,
.button-vue--vue-tertiary:active {
background-color: var(--color-primary-element-light-hover) !important;

}

</style>
20 changes: 20 additions & 0 deletions src/models/contact.js
Original file line number Diff line number Diff line change
Expand Up @@ -527,6 +527,26 @@ export default class Contact {
return this.firstName
}

/**
* Return first matching link for provided type
* Returns empty string otherwise
*
* @param {string} type of social
* @readonly
* @memberof Contact
* @return {string} firstMatchingLink|''
*/
socialLink(type) {
if (this.vCard.hasProperty('x-socialprofile')) {
const x = this.vCard.getAllProperties('x-socialprofile').filter(a => a.jCal[1].type.toString() === type)

if (x.length > 0) {
return x[0].jCal[3].toString()
}
}
return ''
}

/**
* Return the phonetic last name if exists
* Returns the displayName otherwise
Expand Down
1 change: 1 addition & 0 deletions src/models/rfcProps.js
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,7 @@ const properties = {
{ id: 'YOUTUBE', name: 'YouTube', placeholder: 'https://youtube.com/…' },
{ id: 'MASTODON', name: 'Mastodon', placeholder: 'https://mastodon.social/…' },
{ id: 'DIASPORA', name: 'Diaspora', placeholder: 'https://joindiaspora.com/…' },
{ id: 'NEXTCLOUD', name: 'Nextcloud', placeholder: 'Link to profile page (https://nextcloud.example.com/…)' },
{ id: 'OTHER', name: 'Other', placeholder: 'https://example.com/…' },
],
primary: true,
Expand Down
26 changes: 26 additions & 0 deletions src/services/isTalkEnabled.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/**
* @copyright Copyright (c) 2023 Greta Doci <gretadoci@gmail.com>
*
* @author Greta Doci <gretadoci@gmail.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 { loadState } from '@nextcloud/initial-state'

const isTalkEnabled = loadState('contacts', 'isTalkEnabled', false)
Comment thread
GretaD marked this conversation as resolved.
export default isTalkEnabled