mirror of
https://github.com/bringout/oca-ocb-core.git
synced 2026-04-20 20:12:03 +02:00
Initial commit: Core packages
This commit is contained in:
commit
12c29a983b
9512 changed files with 8379910 additions and 0 deletions
|
|
@ -0,0 +1,36 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { useComponentToModel } from '@mail/component_hooks/use_component_to_model';
|
||||
import { useRefToModel } from '@mail/component_hooks/use_ref_to_model';
|
||||
import { registerMessagingComponent } from '@mail/utils/messaging_component';
|
||||
|
||||
import Popover from "web.Popover";
|
||||
import { LegacyComponent } from "@web/legacy/legacy_component";
|
||||
|
||||
export class Activity extends LegacyComponent {
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
setup() {
|
||||
super.setup();
|
||||
useComponentToModel({ fieldName: 'component' });
|
||||
useRefToModel({ fieldName: 'markDoneButtonRef', refName: 'markDoneButton', });
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {ActivityView}
|
||||
*/
|
||||
get activityView() {
|
||||
return this.props.record;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Object.assign(Activity, {
|
||||
props: { record: Object },
|
||||
template: 'mail.Activity',
|
||||
components: { Popover },
|
||||
});
|
||||
|
||||
registerMessagingComponent(Activity);
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
// ------------------------------------------------------------------
|
||||
// Layout
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
.o_Activity_core {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.o_Activity_detailsUserAvatar {
|
||||
object-fit: cover;
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
}
|
||||
|
||||
.o_Activity_iconContainer {
|
||||
@include o-position-absolute($top: auto, $left: auto, $bottom: -16%, $right: -5%);
|
||||
}
|
||||
|
||||
.o_Activity_note p {
|
||||
margin-bottom: map-get($spacers, 0);
|
||||
}
|
||||
|
||||
.o_Activity_sidebar {
|
||||
width: $o-mail-thread-avatar-size;
|
||||
min-width: $o-mail-thread-avatar-size;
|
||||
height: $o-mail-thread-avatar-size;
|
||||
}
|
||||
|
||||
// From python template
|
||||
.o_mail_note_title {
|
||||
margin-top: map-get($spacers, 2);
|
||||
}
|
||||
|
||||
.o_mail_note_title + div p {
|
||||
margin-bottom: map-get($spacers, 0);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Style
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
.o_Activity_detailsButton {
|
||||
@include o-hover-text-color($default-color: $o-main-color-muted);
|
||||
}
|
||||
|
||||
.o_Activity_iconContainer {
|
||||
box-shadow: 0 0 0 2px $o-view-background-color;
|
||||
}
|
||||
|
|
@ -0,0 +1,145 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="mail.Activity" owl="1">
|
||||
<t t-if="activityView">
|
||||
<div class="o_Activity d-flex py-2 px-3" t-attf-class="{{ className }}" t-on-click="activityView.onClickActivity" t-ref="root">
|
||||
<div class="o_Activity_sidebar me-3">
|
||||
<div class="o_Activity_user position-relative h-100 w-100">
|
||||
<t t-if="activityView.activity.assignee">
|
||||
<img class="o_Activity_userAvatar rounded-circle h-100 w-100 o_object_fit_cover" t-attf-src="/web/image/res.users/{{ activityView.activity.assignee.id }}/avatar_128" t-att-alt="activityView.activity.assignee.nameOrDisplayName"/>
|
||||
</t>
|
||||
<div class="o_Activity_iconContainer d-flex align-items-center justify-content-center rounded-circle w-50 h-50"
|
||||
t-att-class="{
|
||||
'text-bg-success': activityView.activity.state === 'planned',
|
||||
'text-bg-warning': activityView.activity.state === 'today',
|
||||
'text-bg-danger': activityView.activity.state === 'overdue',
|
||||
}"
|
||||
>
|
||||
<i class="o_Activity_icon fa small" t-attf-class="{{ activityView.activity.icon }}"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="o_Activity_core">
|
||||
<div class="o_Activity_info d-flex align-items-baseline">
|
||||
<div class="o_Activity_dueDateText me-2"
|
||||
t-att-class="{
|
||||
'text-danger': activityView.activity.state === 'overdue',
|
||||
'text-success': activityView.activity.state === 'planned',
|
||||
'text-warning': activityView.activity.state === 'today',
|
||||
}"
|
||||
>
|
||||
<b t-esc="activityView.delayLabel"/>
|
||||
</div>
|
||||
<t t-if="activityView.activity.summary">
|
||||
<b class="o_Activity_summary text-900 me-2">
|
||||
<t t-esc="activityView.summary"/>
|
||||
</b>
|
||||
</t>
|
||||
<t t-elif="activityView.activity.type">
|
||||
<b class="o_Activity_summary o_Activity_type text-900 me-2">
|
||||
<t t-esc="activityView.activity.type.displayName"/>
|
||||
</b>
|
||||
</t>
|
||||
<t t-if="activityView.activity.assignee">
|
||||
<div class="o_Activity_userName">
|
||||
<t t-esc="activityView.assignedUserText"/>
|
||||
</div>
|
||||
</t>
|
||||
<a
|
||||
href="#"
|
||||
class="o_Activity_detailsButton btn py-0"
|
||||
t-att-class="activityView.areDetailsVisible ? 'text-primary' : 'btn-link btn-primary'"
|
||||
t-att-aria-expanded="activityView.areDetailsVisible ? 'true' : 'false'"
|
||||
t-on-click="activityView.onClickDetailsButton"
|
||||
role="button"
|
||||
>
|
||||
<i class="fa fa-info-circle" role="img" title="Info" aria-label="Info"/>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<t t-if="activityView.areDetailsVisible">
|
||||
<div class="o_Activity_details">
|
||||
<div class="d-md-table table table-sm mt-2 mb-3">
|
||||
<div t-if="activityView.activity.type" class="d-md-table-row mb-3">
|
||||
<div class="d-md-table-cell fw-bold text-md-end m-0 py-md-1 px-md-4">Activity type</div>
|
||||
<div class="o_Activity_type d-md-table-cell py-md-1 pe-4">
|
||||
<t t-esc="activityView.activity.type.displayName"/>
|
||||
</div>
|
||||
</div>
|
||||
<div t-if="activityView.activity.creator" class="d-md-table-row mb-3">
|
||||
<div class="d-md-table-cell fw-bold text-md-end m-0 py-md-1 px-md-4">Created</div>
|
||||
<div class="o_Activity_detailsCreation d-md-table-cell py-md-1 pe-4">
|
||||
<t t-esc="activityView.formattedCreateDatetime"/>, <br t-if="messaging.device.isSmall"/>by
|
||||
<img class="o_Activity_detailsUserAvatar o_Activity_detailsCreatorAvatar ms-1 me-1 rounded-circle align-text-bottom p-0" t-attf-src="/web/image/res.users/{{ activityView.activity.creator.id }}/avatar_128" t-att-title="activityView.activity.creator.nameOrDisplayName" t-att-alt="activityView.activity.creator.nameOrDisplayName"/>
|
||||
<b class="o_Activity_detailsCreator">
|
||||
<t t-esc="activityView.activity.creator.nameOrDisplayName"/>
|
||||
</b>
|
||||
</div>
|
||||
</div>
|
||||
<div t-if="activityView.activity.assignee" class="d-md-table-row mb-3">
|
||||
<div class="d-md-table-cell fw-bold text-md-end m-0 py-md-1 px-md-4">Assigned to</div>
|
||||
<div class="o_Activity_detailsAssignation d-md-table-cell py-md-1 pe-4">
|
||||
<img class="o_Activity_detailsUserAvatar o_Activity_detailsAssignationUserAvatar me-1 rounded-circle align-text-bottom p-0" t-attf-src="/web/image/res.users/{{ activityView.activity.assignee.id }}/avatar_128" t-att-title="activityView.activity.assignee.nameOrDisplayName" t-att-alt="activityView.activity.assignee.nameOrDisplayName"/>
|
||||
<b t-esc="activityView.activity.assignee.nameOrDisplayName"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-md-table-row">
|
||||
<div class="d-md-table-cell fw-bold text-md-end m-0 py-md-1 px-md-4">Due on</div>
|
||||
<div class="o_Activity_detailsDueDate d-md-table-cell py-md-1 pe-4">
|
||||
<span class="o_Activity_deadlineDateText"
|
||||
t-att-class="{
|
||||
'text-danger': activityView.activity.state === 'overdue',
|
||||
'text-success': activityView.activity.state === 'planned',
|
||||
'text-warning': activityView.activity.state === 'today',
|
||||
}"
|
||||
>
|
||||
<t t-esc="activityView.formattedDeadlineDate"/>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<t t-if="activityView.activity.note">
|
||||
<div class="o_Activity_note">
|
||||
<t t-out="activityView.activity.noteAsMarkup"/>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<t t-if="activityView.mailTemplateViews.length > 0">
|
||||
<div class="o_Activity_mailTemplates">
|
||||
<t t-foreach="activityView.mailTemplateViews" t-as="mailTemplateView" t-key="mailTemplateView.localId">
|
||||
<MailTemplate
|
||||
className="'o_Activity_mailTemplate'"
|
||||
record="mailTemplateView"
|
||||
/>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<t t-if="activityView.activity.canWrite">
|
||||
<div name="tools" class="o_Activity_tools d-flex">
|
||||
<button class="o_Activity_toolButton o_Activity_markDoneButton btn btn-link btn-primary pt-0 ps-0" t-att-title="activityView.markDoneText" t-ref="markDoneButton" t-on-click="activityView.onClickMarkDoneButton">
|
||||
<i class="fa fa-check"/> Mark Done
|
||||
</button>
|
||||
<t t-if="activityView.fileUploader">
|
||||
<button class="o_Activity_toolButton o_Activity_uploadButton btn btn-link btn-primary pt-0 ps-0" t-on-click="activityView.onClickUploadDocument">
|
||||
<i class="fa fa-upload"/> Upload Document
|
||||
</button>
|
||||
</t>
|
||||
<button class="o_Activity_toolButton o_Activity_editButton btn btn-link btn-primary pt-0" t-on-click="activityView.onClickEdit">
|
||||
<i class="fa fa-pencil"/> Edit
|
||||
</button>
|
||||
<button class="o_Activity_toolButton o_Activity_cancelButton btn btn-link btn-primary pt-0" t-on-click="activityView.onClickCancel" >
|
||||
<i class="fa fa-times"/> Cancel
|
||||
</button>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { registerMessagingComponent } from '@mail/utils/messaging_component';
|
||||
|
||||
const { Component } = owl;
|
||||
|
||||
export class ActivityBox extends Component {
|
||||
|
||||
/**
|
||||
* @returns {ActivityBoxView}
|
||||
*/
|
||||
get activityBoxView() {
|
||||
return this.props.record;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Object.assign(ActivityBox, {
|
||||
props: { record: Object },
|
||||
template: 'mail.ActivityBox',
|
||||
});
|
||||
|
||||
registerMessagingComponent(ActivityBox);
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
// ------------------------------------------------------------------
|
||||
// Layout
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
.o_ActivityBox_activityList {
|
||||
max-width: var(--Chatter-max-width, none);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Style
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
.o_ActivityBox_title {
|
||||
margin-top: var(--ActivityBox_title-margin, #{map-get($spacers, 4)});
|
||||
}
|
||||
|
||||
.o_ActivityBox_titleLine {
|
||||
border-top: $border-width dashed $border-color;
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="mail.ActivityBox" owl="1">
|
||||
<t t-if="activityBoxView">
|
||||
<div class="o_ActivityBox" t-attf-class="{{ className }}" t-ref="root">
|
||||
<a href="#" role="button" class="o_ActivityBox_title btn d-flex align-items-center p-0 w-100 fw-bold" t-att-aria-expanded="activityBoxView.isActivityListVisible ? 'true' : 'false'" t-on-click="activityBoxView.onClickActivityBoxTitle">
|
||||
<hr class="o_ActivityBox_titleLine w-auto flex-grow-1 me-3" />
|
||||
<span class="o_ActivityBox_titleText">
|
||||
<i class="fa fa-fw" t-att-class="activityBoxView.isActivityListVisible ? 'fa-caret-down' : 'fa-caret-right'"/>
|
||||
Planned activities
|
||||
</span>
|
||||
<t t-if="!activityBoxView.isActivityListVisible">
|
||||
<span class="o_ActivityBox_titleBadges ms-2">
|
||||
<t t-if="activityBoxView.chatter.thread.overdueActivities.length > 0">
|
||||
<span class="o_ActivityBox_titleBadge me-1 badge text-bg-danger">
|
||||
<t t-esc="activityBoxView.chatter.thread.overdueActivities.length"/>
|
||||
</span>
|
||||
</t>
|
||||
<t t-if="activityBoxView.chatter.thread.todayActivities.length > 0">
|
||||
<span class="o_ActivityBox_titleBadge me-1 badge text-bg-warning">
|
||||
<t t-esc="activityBoxView.chatter.thread.todayActivities.length"/>
|
||||
</span>
|
||||
</t>
|
||||
<t t-if="activityBoxView.chatter.thread.futureActivities.length > 0">
|
||||
<span class="o_ActivityBox_titleBadge me-1 badge text-bg-success">
|
||||
<t t-esc="activityBoxView.chatter.thread.futureActivities.length"/>
|
||||
</span>
|
||||
</t>
|
||||
</span>
|
||||
</t>
|
||||
<hr class="o_ActivityBox_titleLine w-auto flex-grow-1 ms-3"/>
|
||||
</a>
|
||||
<t t-if="activityBoxView.isActivityListVisible">
|
||||
<div class="o_ActivityBox_activityList">
|
||||
<t t-foreach="activityBoxView.activityViews" t-as="activityView" t-key="activityView.localId">
|
||||
<Activity className="'o_ActivityBox_activity'" record="activityView"/>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { useComponentToModel } from '@mail/component_hooks/use_component_to_model';
|
||||
import { useRefToModel } from '@mail/component_hooks/use_ref_to_model';
|
||||
import { registerMessagingComponent } from '@mail/utils/messaging_component';
|
||||
import { LegacyComponent } from "@web/legacy/legacy_component";
|
||||
|
||||
const { onMounted, useRef } = owl;
|
||||
|
||||
export class ActivityMarkDonePopoverContent extends LegacyComponent {
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
setup() {
|
||||
super.setup();
|
||||
useComponentToModel({ fieldName: 'component' });
|
||||
useRefToModel({ fieldName: 'feedbackTextareaRef', refName: 'feedbackTextarea' });
|
||||
this._feedbackTextareaRef = useRef('feedbackTextarea');
|
||||
onMounted(() => this._mounted());
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Public
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
_mounted() {
|
||||
this._feedbackTextareaRef.el.focus();
|
||||
if (this.activityMarkDonePopoverContentView.activity.feedbackBackup) {
|
||||
this._feedbackTextareaRef.el.value = this.activityMarkDonePopoverContentView.activity.feedbackBackup;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {ActivityMarkDonePopoverContentView}
|
||||
*/
|
||||
get activityMarkDonePopoverContentView() {
|
||||
return this.props.record;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Object.assign(ActivityMarkDonePopoverContent, {
|
||||
props: { record: Object },
|
||||
template: 'mail.ActivityMarkDonePopoverContent',
|
||||
});
|
||||
|
||||
registerMessagingComponent(ActivityMarkDonePopoverContent);
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
// ------------------------------------------------------------------
|
||||
// Layout
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
.o_ActivityMarkDonePopoverContent_feedback {
|
||||
min-height: 70px;
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="mail.ActivityMarkDonePopoverContent" owl="1">
|
||||
<t t-if="activityMarkDonePopoverContentView">
|
||||
<div class="o_ActivityMarkDonePopoverContent" t-attf-class="{{ className }}" t-on-keydown="activityMarkDonePopoverContentView.onKeydown" t-ref="root">
|
||||
<h6 t-if="activityMarkDonePopoverContentView.hasHeader" class="o_ActivityMarkDonePopoverContent_header p-2 fw-bolder bg-200 border-bottom" t-esc="activityMarkDonePopoverContentView.headerText"/>
|
||||
<div class="o_ActivityMarkDonePopoverContent_content py-2 px-3">
|
||||
<textarea class="form-control o_ActivityMarkDonePopoverContent_feedback" rows="3" placeholder="Write Feedback" t-on-blur="activityMarkDonePopoverContentView.onBlur" t-ref="feedbackTextarea"/>
|
||||
<div class="o_ActivityMarkDonePopoverContent_buttons mt-2">
|
||||
<button type="button" class="o_ActivityMarkDonePopoverContent_doneScheduleNextButton btn btn-sm btn-primary" t-on-click="activityMarkDonePopoverContentView.onClickDoneAndScheduleNext">
|
||||
Done & Schedule Next
|
||||
</button>
|
||||
<t t-if="activityMarkDonePopoverContentView.activity.chaining_type === 'suggest'">
|
||||
<button type="button" class="o_ActivityMarkDonePopoverContent_doneButton btn btn-sm btn-primary mx-2" t-on-click="activityMarkDonePopoverContentView.onClickDone">
|
||||
Done
|
||||
</button>
|
||||
</t>
|
||||
<button type="button" class="o_ActivityMarkDonePopoverContent_discardButton btn btn-sm btn-link" t-on-click="activityMarkDonePopoverContentView.onClickDiscard">
|
||||
Discard
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
// ensure components are registered beforehand.
|
||||
import '@mail/components/activity_menu_view/activity_menu_view';
|
||||
import { getMessagingComponent } from "@mail/utils/messaging_component";
|
||||
|
||||
const { Component } = owl;
|
||||
|
||||
export class ActivityMenuContainer extends Component {
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
setup() {
|
||||
super.setup();
|
||||
this.env.services.messaging.modelManager.messagingCreatedPromise.then(() => {
|
||||
this.activityMenuView = this.env.services.messaging.modelManager.messaging.models['ActivityMenuView'].insert();
|
||||
this.render();
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Object.assign(ActivityMenuContainer, {
|
||||
components: { ActivityMenuView: getMessagingComponent('ActivityMenuView') },
|
||||
template: 'mail.ActivityMenuContainer',
|
||||
});
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
<t t-name="mail.ActivityMenuContainer" owl="1">
|
||||
<t t-if="activityMenuView">
|
||||
<ActivityMenuView record="activityMenuView"/>
|
||||
</t>
|
||||
</t>
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { useComponentToModel } from '@mail/component_hooks/use_component_to_model';
|
||||
import { registerMessagingComponent } from '@mail/utils/messaging_component';
|
||||
|
||||
const { Component } = owl;
|
||||
|
||||
export class ActivityMenuView extends Component {
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
setup() {
|
||||
super.setup();
|
||||
useComponentToModel({ fieldName: 'component' });
|
||||
}
|
||||
/**
|
||||
* @returns {ActivityMenuView}
|
||||
*/
|
||||
get activityMenuView() {
|
||||
return this.props.record;
|
||||
}
|
||||
}
|
||||
|
||||
Object.assign(ActivityMenuView, {
|
||||
props: { record: Object },
|
||||
template: 'mail.ActivityMenuView',
|
||||
});
|
||||
|
||||
registerMessagingComponent(ActivityMenuView);
|
||||
|
|
@ -0,0 +1,136 @@
|
|||
.o_ActivityMenuView_activityGroups {
|
||||
flex: 0 1 auto;
|
||||
max-height: 400px;
|
||||
min-height: 50px;
|
||||
overflow-y: auto;
|
||||
|
||||
@include media-breakpoint-down(md) {
|
||||
max-height: none;
|
||||
padding-bottom: 52px; // leave space for tabs
|
||||
}
|
||||
}
|
||||
|
||||
.o_ActivityMenuView_activityGroup {
|
||||
display: flex;
|
||||
background-color: transparent;
|
||||
color: $o-main-text-color;
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
&:hover {
|
||||
background-color: map-get($theme-colors, 'light');
|
||||
.o_ActivityMenuView_activityGroupName {
|
||||
color: $headings-color;
|
||||
}
|
||||
}
|
||||
&:not(:last-child) {
|
||||
border-bottom: 1px solid map-get($grays, '400');
|
||||
}
|
||||
@include media-breakpoint-down(md) {
|
||||
padding: $o-mail-chatter-mobile-gap;
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
min-height: 50px;
|
||||
padding: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.o_ActivityMenuView_activityGroupActionButtons {
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
flex-flow: row-reverse wrap;
|
||||
}
|
||||
|
||||
.o_ActivityMenuView_activityGroupActionButton {
|
||||
padding-top: 0px;
|
||||
padding-bottom: 0px;
|
||||
padding-right: 0px;
|
||||
}
|
||||
|
||||
.o_ActivityMenuView_activityGroupFilterButton {
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
.o_ActivityMenuView_activityGroupIconContainer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 0 0 auto;
|
||||
position: relative;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
object-fit: cover;
|
||||
> img {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
object-fit: cover;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
.fa-circle-o {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.o_ActivityMenuView_activityGroupInfo {
|
||||
flex: 1 1 100%;
|
||||
overflow: hidden;
|
||||
|
||||
@include media-breakpoint-down(md) {
|
||||
margin-left: $o-mail-chatter-mobile-gap;
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.o_ActivityMenuView_activityGroupName {
|
||||
flex: 0 1 auto;
|
||||
@include o-text-overflow;
|
||||
|
||||
@include media-breakpoint-down(md) {
|
||||
font-size: 1.1em;
|
||||
}
|
||||
}
|
||||
|
||||
.o_ActivityMenuView_activityGroupNoCount {
|
||||
cursor: initial;
|
||||
align-items: center;
|
||||
opacity: 0.5;
|
||||
padding: 3px;
|
||||
min-height: inherit;
|
||||
}
|
||||
|
||||
.o_ActivityMenuView_activityGroupTitle {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.o_ActivityMenuView_dropdownMenu {
|
||||
direction: ltr;
|
||||
width: 350px;
|
||||
padding: 0;
|
||||
|
||||
@include media-breakpoint-down(md) {
|
||||
position: fixed;
|
||||
top: $o-mail-chat-window-header-height-mobile;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
max-height: none;
|
||||
}
|
||||
}
|
||||
|
||||
.o_ActivityMenuView_noActivity {
|
||||
cursor: initial;
|
||||
align-items: center;
|
||||
color: grey;
|
||||
opacity: 0.5;
|
||||
padding: 3px;
|
||||
min-height: inherit;
|
||||
}
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="mail.ActivityMenuView" owl="1">
|
||||
<div class="o_ActivityMenuView dropdown" t-ref="root">
|
||||
<a class="o_ActivityMenuView_dropdownToggle dropdown-toggle o-no-caret o-dropdown--narrow" t-att-aria-expanded="activityMenuView.isOpen ? 'true' : 'false'" title="Activities" href="#" role="button" t-on-click="activityMenuView.onClickDropdownToggle">
|
||||
<i class="fa fa-lg fa-clock-o" role="img" aria-label="Activities"/> <span t-if="activityMenuView.counter > 0" class="o_ActivityMenuView_counter badge" t-esc="activityMenuView.counter"/>
|
||||
</a>
|
||||
<div t-if="activityMenuView.isOpen" class="o_ActivityMenuView_dropdownMenu o-dropdown-menu dropdown-menu-end show bg-view" role="menu">
|
||||
<div class="o_ActivityMenuView_activityGroups">
|
||||
<t t-if="activityMenuView.activityGroupViews.length === 0">
|
||||
<div class="o_ActivityMenuView_noActivity dropdown-item-text text-center d-flex justify-content-center">
|
||||
<span>Congratulations, you're done with your activities.</span>
|
||||
</div>
|
||||
</t>
|
||||
<t t-foreach="activityMenuView.activityGroupViews" t-as="activityGroupView" t-key="activityGroupView.localId" name="activityGroupLoop">
|
||||
<div class="o_ActivityMenuView_activityGroup" t-att-data-res_model="activityGroupView.activityGroup.irModel.model" t-att-data-model_name="activityGroupView.activityGroup.irModel.name" t-att-data-domain="activityGroupView.activityGroup.domain" data-filter='my' t-att-data-activity-group-view-local-id="activityGroupView.localId" t-on-click="activityGroupView.onClickFilterButton">
|
||||
<div t-if="activityGroupView.activityGroup.irModel.iconUrl" class="o_ActivityMenuView_activityGroupIconContainer">
|
||||
<img t-att-src="activityGroupView.activityGroup.irModel.iconUrl" alt="Activity"/>
|
||||
</div>
|
||||
<div class="o_ActivityMenuView_activityGroupInfo">
|
||||
<div class="o_ActivityMenuView_activityGroupTitle">
|
||||
<span class="o_ActivityMenuView_activityGroupName">
|
||||
<t t-esc="activityGroupView.activityGroup.irModel.name"/>
|
||||
</span>
|
||||
<div t-if="activityGroupView.activityGroup.actions" class="o_ActivityMenuView_activityGroupActionButtons">
|
||||
<t t-foreach="activityGroupView.activityGroup.actions" t-as="action" t-key="action.name">
|
||||
<button type="button"
|
||||
t-att-title="action.name"
|
||||
t-att-class="'o_ActivityMenuView_activityGroupActionButton btn btn-link fa ' + action.icon"
|
||||
t-att-data-action_xmlid="action.action_xmlid"
|
||||
t-att-data-res_model="activityGroupView.activityGroup.irModel.model"
|
||||
t-att-data-model_name="activityGroupView.activityGroup.irModel.name"
|
||||
t-att-data-domain="activityGroupView.activityGroup.domain"
|
||||
t-on-click="activityGroupView.onClick"
|
||||
>
|
||||
</button>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
<div t-if="activityGroupView.activityGroup.type == 'activity'">
|
||||
<button t-if="activityGroupView.activityGroup.overdue_count" type="button" class="o_ActivityMenuView_activityGroupFilterButton btn btn-link mr16" t-att-data-res_model="activityGroupView.activityGroup.irModel.model" t-att-data-model_name="activityGroupView.activityGroup.irModel.name" data-filter='overdue'><t t-esc="activityGroupView.activityGroup.overdue_count"/> Late </button>
|
||||
<span t-if="!activityGroupView.activityGroup.overdue_count" class="o_ActivityMenuView_activityGroupNoCount mr16 text-muted">0 Late </span>
|
||||
<button t-if="activityGroupView.activityGroup.today_count" type="button" class="o_ActivityMenuView_activityGroupFilterButton btn btn-link mr16" t-att-data-res_model="activityGroupView.activityGroup.irModel.model" t-att-data-model_name="activityGroupView.activityGroup.irModel.name" data-filter='today'> <t t-esc="activityGroupView.activityGroup.today_count"/> Today </button>
|
||||
<span t-if="!activityGroupView.activityGroup.today_count" class="o_ActivityMenuView_activityGroupNoCount mr16 text-muted">0 Today </span>
|
||||
<button t-if="activityGroupView.activityGroup.planned_count" type="button" class="o_ActivityMenuView_activityGroupFilterButton btn btn-link float-end" t-att-data-res_model="activityGroupView.activityGroup.irModel.model" t-att-data-model_name="activityGroupView.activityGroup.irModel.name" data-filter='upcoming_all'> <t t-esc="activityGroupView.activityGroup.planned_count"/> Future </button>
|
||||
<span t-if="!activityGroupView.activityGroup.planned_count" class="o_ActivityMenuView_activityGroupNoCount float-end text-muted">0 Future</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { useComponentToModel } from '@mail/component_hooks/use_component_to_model';
|
||||
import { registerMessagingComponent } from '@mail/utils/messaging_component';
|
||||
|
||||
const { Component } = owl;
|
||||
|
||||
export class AttachmentBox extends Component {
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
setup() {
|
||||
super.setup();
|
||||
useComponentToModel({ fieldName: 'component' });
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Public
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @returns {AttachmentBoxView|undefined}
|
||||
*/
|
||||
get attachmentBoxView() {
|
||||
return this.props.record;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Object.assign(AttachmentBox, {
|
||||
props: { record: Object },
|
||||
template: 'mail.AttachmentBox',
|
||||
});
|
||||
|
||||
registerMessagingComponent(AttachmentBox);
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
// ------------------------------------------------------------------
|
||||
// Style
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
.o_AttachmentBox_dashedLine {
|
||||
border-top: $border-width dashed $border-color;
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="mail.AttachmentBox" owl="1">
|
||||
<t t-if="attachmentBoxView">
|
||||
<div class="o_AttachmentBox position-relative" t-attf-class="{{ className }}" t-ref="root">
|
||||
<div class="o_AttachmentBox_title d-flex align-items-center">
|
||||
<hr class="o_AttachmentBox_dashedLine flex-grow-1"/>
|
||||
<span class="o_AttachmentBox_titleText p-3 fw-bold">
|
||||
Files
|
||||
</span>
|
||||
<hr class="o_AttachmentBox_dashedLine flex-grow-1"/>
|
||||
</div>
|
||||
<div class="o_AttachmentBox_content d-flex flex-column">
|
||||
<t t-if="attachmentBoxView.attachmentList">
|
||||
<AttachmentList
|
||||
className="'o_attachmentBox_attachmentList'"
|
||||
record="attachmentBoxView.attachmentList"
|
||||
/>
|
||||
</t>
|
||||
<button class="o_AttachmentBox_buttonAdd btn btn-link" type="button" t-on-click="attachmentBoxView.onClickAddAttachment" t-att-disabled="!attachmentBoxView.chatter.isTemporary and !attachmentBoxView.chatter.hasWriteAccess">
|
||||
<i class="fa fa-plus-square"/>
|
||||
Attach files
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { registerMessagingComponent } from '@mail/utils/messaging_component';
|
||||
|
||||
const { Component } = owl;
|
||||
|
||||
export class AttachmentCard extends Component {
|
||||
|
||||
/**
|
||||
* @returns {AttachmentCard}
|
||||
*/
|
||||
get attachmentCard() {
|
||||
return this.props.record;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Object.assign(AttachmentCard, {
|
||||
props: { record: Object },
|
||||
template: 'mail.AttachmentCard',
|
||||
});
|
||||
|
||||
registerMessagingComponent(AttachmentCard);
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
// ------------------------------------------------------------------
|
||||
// Layout
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
.o_AttachmentCard:hover .o_AttachmentCard_asideItemUnlink.o-pretty {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.o_AttachmentCard_action {
|
||||
min-width: 20px;
|
||||
}
|
||||
|
||||
.o_AttachmentCard_aside {
|
||||
&:not(.o-hasMultipleActions) {
|
||||
min-width: 50px;
|
||||
}
|
||||
|
||||
&.o-hasMultipleActions {
|
||||
min-width: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
.o_AttachmentCard_asideItemUnlink.o-pretty {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
|
||||
.o_AttachmentCard_details {
|
||||
min-width: 0; /* This allows the text ellipsis in the flex element */
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Style
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
.o_AttachmentCard_image.o-attachment-viewable {
|
||||
cursor: zoom-in;
|
||||
}
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
<t t-name="mail.AttachmentCard" owl="1">
|
||||
<t t-if="attachmentCard">
|
||||
<div t-attf-class="{{ className }}" t-ref="root">
|
||||
<div class="o_AttachmentCard o-has-card-details d-flex rounded bg-300"
|
||||
t-att-class="{
|
||||
'o-isUploading': attachmentCard.attachment.isUploading,
|
||||
'o-viewable': attachmentCard.attachment.isViewable,
|
||||
}" t-att-title="attachmentCard.attachment.displayName ? attachmentCard.attachment.displayName : undefined" role="menu" t-att-aria-label="attachmentCard.attachment.displayName" t-att-data-id="attachmentCard.attachment.localId"
|
||||
>
|
||||
<!-- Image style-->
|
||||
<!-- o_image from mimetype.scss -->
|
||||
<div class="o_AttachmentCard_image o_image flex-shrink-0 m-1" t-on-click="attachmentCard.onClickImage" t-att-class="{'o-attachment-viewable opacity-75-hover': attachmentCard.attachment.isViewable,}" role="menuitem" aria-label="Preview" t-att-tabindex="attachmentCard.attachment.isViewable ? 0 : -1" t-att-aria-disabled="!attachmentCard.attachment.isViewable" t-att-data-mimetype="attachmentCard.attachment.mimetype">
|
||||
</div>
|
||||
<!-- Attachment details -->
|
||||
<div class="o_AttachmentCard_details d-flex justify-content-center flex-column px-1">
|
||||
<t t-if="attachmentCard.attachment.displayName">
|
||||
<div class="o_AttachmentCard_filename text-truncate">
|
||||
<t t-esc="attachmentCard.attachment.displayName"/>
|
||||
</div>
|
||||
</t>
|
||||
<t t-if="attachmentCard.attachment.extension">
|
||||
<small class="o_AttachmentCard_extension text-uppercase">
|
||||
<t t-esc="attachmentCard.attachment.extension"/>
|
||||
</small>
|
||||
</t>
|
||||
</div>
|
||||
<!-- Attachment aside -->
|
||||
<div class="o_AttachmentCard_aside position-relative rounded-end overflow-hidden" t-att-class="{ 'o-hasMultipleActions d-flex flex-column': attachmentCard.hasMultipleActions }">
|
||||
<!-- Uploading icon -->
|
||||
<t t-if="attachmentCard.attachment.isUploading and attachmentCard.attachmentList.composerViewOwner">
|
||||
<div class="o_AttachmentCard_asideItem o_AttachmentCard_asideItemUploading d-flex justify-content-center align-items-center w-100 h-100" title="Uploading">
|
||||
<i class="fa fa-spin fa-spinner"/>
|
||||
</div>
|
||||
</t>
|
||||
<!-- Uploaded icon -->
|
||||
<t t-if="!attachmentCard.attachment.isUploading and attachmentCard.attachmentList.composerViewOwner">
|
||||
<div class="o_AttachmentCard_asideItem o_AttachmentCard_asideItemUploaded d-flex justify-content-center align-items-center w-100 h-100 text-primary" title="Uploaded">
|
||||
<i class="fa fa-check"/>
|
||||
</div>
|
||||
</t>
|
||||
<!-- Remove button -->
|
||||
<t t-if="attachmentCard.attachment.isDeletable">
|
||||
<button class="o_AttachmentCard_asideItem o_AttachmentCard_asideItemUnlink btn top-0 justify-content-center align-items-center d-flex w-100 h-100 rounded-0" t-attf-class="{{ attachmentCard.attachmentList.composerViewOwner ? 'o-pretty position-absolute btn-primary transition-base' : 'bg-300' }}" t-on-click="attachmentCard.onClickUnlink" title="Remove">
|
||||
<i class="fa fa-trash" role="img" aria-label="Remove"/>
|
||||
</button>
|
||||
</t>
|
||||
<!-- Open link button -->
|
||||
<t t-if="attachmentCard.attachment.type === 'url'">
|
||||
<a class="o_AttachmentCard_asideItem o_AttachmentCard_asideItemOpenLink btn d-flex justify-content-center align-items-center w-100 h-100 rounded-0 bg-300" t-att-href="attachmentCard.attachment.url" target='_blank' title="Open Link">
|
||||
<i class="fa fa-external-link" role="img" aria-label="Open Link"/>
|
||||
</a>
|
||||
</t>
|
||||
<!-- Download button -->
|
||||
<t t-elif="!attachmentCard.attachmentList.composerViewOwner and !attachmentCard.attachment.isUploading">
|
||||
<button class="o_AttachmentCard_asideItem o_AttachmentCard_asideItemDownload btn d-flex justify-content-center align-items-center w-100 h-100 rounded-0 bg-300" t-on-click="attachmentCard.attachment.onClickDownload" title="Download">
|
||||
<i class="fa fa-download" role="img" aria-label="Download"/>
|
||||
</button>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { useComponentToModel } from '@mail/component_hooks/use_component_to_model';
|
||||
import { registerMessagingComponent } from '@mail/utils/messaging_component';
|
||||
|
||||
const { Component } = owl;
|
||||
|
||||
export class AttachmentDeleteConfirm extends Component {
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
setup() {
|
||||
useComponentToModel({ fieldName: 'component' });
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {AttachmentDeleteConfirmView}
|
||||
*/
|
||||
get attachmentDeleteConfirmView() {
|
||||
return this.props.record;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Object.assign(AttachmentDeleteConfirm, {
|
||||
props: { record: Object },
|
||||
template: 'mail.AttachmentDeleteConfirm',
|
||||
});
|
||||
|
||||
registerMessagingComponent(AttachmentDeleteConfirm);
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
<t t-name="mail.AttachmentDeleteConfirm" owl="1">
|
||||
<t t-if="attachmentDeleteConfirmView">
|
||||
<div class="o_AttachmentDeleteConfirm card bg-view" t-attf-class="{{ className }}" t-ref="root">
|
||||
<h4 class="m-3">Confirmation</h4>
|
||||
<hr class="mt-0 mb-3"/>
|
||||
<p class="o_AttachmentDeleteConfirm_mainText mx-3 mb-3" t-esc="attachmentDeleteConfirmView.body"/>
|
||||
<hr class="mt-0 mb-3"/>
|
||||
<div class="o_AttachmentDeleteConfirm_buttons mx-3 mb-3">
|
||||
<button class="o_AttachmentDeleteConfirm_confirmButton btn btn-primary me-2" t-on-click="attachmentDeleteConfirmView.onClickOk">Ok</button>
|
||||
<button class="o_AttachmentDeleteConfirm_cancelButton btn btn-secondary me-2" t-on-click="attachmentDeleteConfirmView.onClickCancel">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { registerMessagingComponent } from '@mail/utils/messaging_component';
|
||||
|
||||
const { Component } = owl;
|
||||
|
||||
export class AttachmentImage extends Component {
|
||||
|
||||
/**
|
||||
* @returns {AttachmentImage}
|
||||
*/
|
||||
get attachmentImage() {
|
||||
return this.props.record;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Object.assign(AttachmentImage, {
|
||||
props: { record: Object },
|
||||
template: 'mail.AttachmentImage',
|
||||
});
|
||||
|
||||
registerMessagingComponent(AttachmentImage);
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
// ------------------------------------------------------------------
|
||||
// Layout
|
||||
// ------------------------------------------------------------------
|
||||
.o_AttachmentImage {
|
||||
min-width: 20px;
|
||||
min-height: 20px;
|
||||
|
||||
img {
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Style
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
.o_AttachmentImage {
|
||||
cursor: zoom-in;
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
<t t-name="mail.AttachmentImage" owl="1">
|
||||
<t t-if="attachmentImage">
|
||||
<div t-attf-class="{{ className }}" role="menu" t-att-aria-label="attachmentImage.attachment.displayName" t-ref="root">
|
||||
<div class="o_AttachmentImage d-flex position-relative flex-shrink-0"
|
||||
t-att-class="{
|
||||
'o-isUploading': attachmentImage.attachment.isUploading,
|
||||
}"
|
||||
t-att-title="attachmentImage.attachment.displayName ? attachmentImage.attachment.displayName : undefined"
|
||||
t-att-data-id="attachmentImage.attachment.localId"
|
||||
tabindex="0"
|
||||
aria-label="View image"
|
||||
role="menuitem"
|
||||
t-on-click="attachmentImage.onClickImage"
|
||||
t-att-data-mimetype="attachmentImage.attachment.mimetype"
|
||||
>
|
||||
<t t-if="!attachmentImage.attachment.isUploading">
|
||||
<img class="img img-fluid my-0 mx-auto" t-att-src="attachmentImage.imageUrl" t-att-alt="attachmentImage.attachment.name" t-attf-style="max-width: min(100%, {{ attachmentImage.width }}px); max-height: {{ attachmentImage.height }}px;"/>
|
||||
</t>
|
||||
<t t-if="attachmentImage.attachment.isUploading">
|
||||
<div class="o_AttachmentImageUploading position-absolute top-0 bottom-0 start-0 end-0 d-flex align-items-center justify-content-center" title="Uploading">
|
||||
<i class="fa fa-spin fa-spinner"/>
|
||||
</div>
|
||||
</t>
|
||||
<div class="o_AttachmentImage_imageOverlay position-absolute top-0 bottom-0 start-0 end-0 p-2 text-white opacity-0 opacity-100-hover d-flex align-items-end flax-wrap flex-column">
|
||||
<div t-if="attachmentImage.attachment.isDeletable" class="o_AttachmentImage_action o_AttachmentImage_actionUnlink btn btn-sm btn-dark rounded opacity-75 opacity-100-hover" t-att-class="{'o-pretty': attachmentImage.attachmentList.composerViewOwner}" tabindex="0" aria-label="Remove" role="menuitem" t-on-click="attachmentImage.onClickUnlink" title="Remove">
|
||||
<i class="fa fa-trash"/>
|
||||
</div>
|
||||
<div t-if="attachmentImage.hasDownloadButton" class="o_AttachmentImage_action o_AttachmentImage_actionDownload btn btn-sm btn-dark rounded opacity-75 opacity-100-hover mt-auto" t-on-click="attachmentImage.onClickDownload" title="Download">
|
||||
<i class="fa fa-download"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { registerMessagingComponent } from '@mail/utils/messaging_component';
|
||||
|
||||
const { Component } = owl;
|
||||
|
||||
export class AttachmentList extends Component {
|
||||
|
||||
/**
|
||||
* @returns {AttachmentList}
|
||||
*/
|
||||
get attachmentList() {
|
||||
return this.props.record;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Object.assign(AttachmentList, {
|
||||
props: { record: Object },
|
||||
template: 'mail.AttachmentList',
|
||||
});
|
||||
|
||||
registerMessagingComponent(AttachmentList);
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
.o_AttachmentList {
|
||||
max-width: var(--Chatter-max-width, none);
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="mail.AttachmentList" owl="1">
|
||||
<t t-if="attachmentList">
|
||||
<div class="o_AttachmentList d-flex flex-column mt-1" t-att-class="{ 'me-2 pe-4': attachmentList.isInChatWindowAndIsAlignedLeft and !attachmentList.composerViewOwner, 'ms-2 ps-4': attachmentList.isInChatWindowAndIsAlignedRight and !attachmentList.composerViewOwner }" t-attf-class="{{ className }}" t-ref="root">
|
||||
<div t-if="attachmentList.attachmentImages.length > 0" class="o_AttachmentList_partialList o_AttachmentList_partialListImages d-flex flex-grow-1 flex-wrap" t-att-class="{ 'justify-content-end': attachmentList.isInChatWindowAndIsAlignedRight and !attachmentList.composerViewOwner }">
|
||||
<t t-foreach="attachmentList.attachmentImages" t-as="attachmentImage" t-key="attachmentImage.localId">
|
||||
<AttachmentImage className="'o_AttachmentList_attachment mw-100 mb-1'" classNameObj="{ 'ms-1': attachmentList.isInChatWindowAndIsAlignedRight, 'me-1': !attachmentList.isInChatWindowAndIsAlignedRight }" record="attachmentImage"/>
|
||||
</t>
|
||||
</div>
|
||||
<div t-if="attachmentList.attachmentCards.length > 0" class="o_AttachmentList_partialList o_AttachmentList_partialListNonImages d-flex flex-grow-1 flex-wrap mt-1" t-att-class="{ 'justify-content-end': attachmentList.isInChatWindowAndIsAlignedRight and !attachmentList.composerViewOwner }">
|
||||
<t t-foreach="attachmentList.attachmentCards" t-as="attachmentCard" t-key="attachmentCard.localId">
|
||||
<AttachmentCard className="'o_AttachmentList_attachment mw-100 mb-1'" classNameObj="{ 'ms-1': attachmentList.isInChatWindowAndIsAlignedRight, 'me-1': !attachmentList.isInChatWindowAndIsAlignedRight }" record="attachmentCard"/>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
// = Attachment Viewer View
|
||||
// ============================================================================
|
||||
// No CSS hacks, variables overrides only
|
||||
|
||||
.o_AttachmentViewer_toolbarButton {
|
||||
--AttachmentViewer_toolbarButton-background-color: #{$o-gray-200};
|
||||
}
|
||||
|
|
@ -0,0 +1,359 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { useComponentToModel } from '@mail/component_hooks/use_component_to_model';
|
||||
import { useRefs } from '@mail/component_hooks/use_refs';
|
||||
import { registerMessagingComponent } from '@mail/utils/messaging_component';
|
||||
import { hidePDFJSButtons } from '@web/legacy/js/libs/pdfjs';
|
||||
|
||||
const { Component, onMounted, onPatched, onWillUnmount, useRef } = owl;
|
||||
|
||||
const MIN_SCALE = 0.5;
|
||||
const SCROLL_ZOOM_STEP = 0.1;
|
||||
const ZOOM_STEP = 0.5;
|
||||
|
||||
export class AttachmentViewer extends Component {
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
setup() {
|
||||
super.setup();
|
||||
useComponentToModel({ fieldName: 'component' });
|
||||
this.MIN_SCALE = MIN_SCALE;
|
||||
/**
|
||||
* Used to ensure that the ref is always up to date, which seems to be needed if the element
|
||||
* has a t-key, which was added to force the rendering of a new element when the src of the image changes.
|
||||
* This was made to remove the display of the previous image as soon as the src changes.
|
||||
*/
|
||||
this._getRefs = useRefs();
|
||||
/**
|
||||
* Reference of the zoomer node. Useful to apply translate
|
||||
* transformation on image visualisation.
|
||||
*/
|
||||
this._zoomerRef = useRef('zoomer');
|
||||
/**
|
||||
* Reference of the IFRAME node when the attachment is a PDF.
|
||||
*/
|
||||
this._iframeViewerPdfRef = useRef('iframeViewerPdf');
|
||||
/**
|
||||
* Tracked translate transformations on image visualisation. This is
|
||||
* not observed for re-rendering because they are used to compute zoomer
|
||||
* style, and this is changed directly on zoomer for performance
|
||||
* reasons (overhead of making vdom is too significant for each mouse
|
||||
* position changes while dragging)
|
||||
*/
|
||||
this._translate = { x: 0, y: 0, dx: 0, dy: 0 };
|
||||
this._onClickGlobal = this._onClickGlobal.bind(this);
|
||||
onMounted(() => this._mounted());
|
||||
onPatched(() => this._patched());
|
||||
onWillUnmount(() => this._willUnmount());
|
||||
}
|
||||
|
||||
_mounted() {
|
||||
if (!this.root.el) {
|
||||
return;
|
||||
}
|
||||
this.root.el.focus();
|
||||
this._handleImageLoad();
|
||||
this._hideUnwantedPdfJsButtons();
|
||||
document.addEventListener('click', this._onClickGlobal);
|
||||
}
|
||||
|
||||
/**
|
||||
* When a new image is displayed, show a spinner until it is loaded.
|
||||
*/
|
||||
_patched() {
|
||||
this._handleImageLoad();
|
||||
this._hideUnwantedPdfJsButtons();
|
||||
}
|
||||
|
||||
_willUnmount() {
|
||||
document.removeEventListener('click', this._onClickGlobal);
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Public
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @returns {AttachmentViewer}
|
||||
*/
|
||||
get attachmentViewer() {
|
||||
return this.props.record;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Determine whether the current image is rendered for the 1st time, and if
|
||||
* that's the case, display a spinner until loaded.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_handleImageLoad() {
|
||||
if (!this.attachmentViewer.exists() || !this.attachmentViewer.attachmentViewerViewable) {
|
||||
return;
|
||||
}
|
||||
const refs = this._getRefs();
|
||||
const image = refs[`image_${this.attachmentViewer.attachmentViewerViewable.localId}`];
|
||||
if (
|
||||
this.attachmentViewer.attachmentViewerViewable.isImage &&
|
||||
(!image || !image.complete)
|
||||
) {
|
||||
this.attachmentViewer.update({ isImageLoading: true });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @see 'hidePDFJSButtons'
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_hideUnwantedPdfJsButtons() {
|
||||
if (this._iframeViewerPdfRef.el) {
|
||||
hidePDFJSButtons(this._iframeViewerPdfRef.el);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop dragging interaction of the user.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_stopDragging() {
|
||||
this.attachmentViewer.update({ isDragging: false });
|
||||
this._translate.x += this._translate.dx;
|
||||
this._translate.y += this._translate.dy;
|
||||
this._translate.dx = 0;
|
||||
this._translate.dy = 0;
|
||||
this._updateZoomerStyle();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the style of the zoomer based on translate transformation. Changes
|
||||
* are directly applied on zoomer, instead of triggering re-render and
|
||||
* defining them in the template, for performance reasons.
|
||||
*
|
||||
* @private
|
||||
* @returns {string}
|
||||
*/
|
||||
_updateZoomerStyle() {
|
||||
const attachmentViewer = this.attachmentViewer;
|
||||
const refs = this._getRefs();
|
||||
const image = refs[`image_${this.attachmentViewer.attachmentViewerViewable.localId}`];
|
||||
// some actions are too fast that sometimes this function is called
|
||||
// before setting the refs, so we just do nothing when image is null
|
||||
if (!image) {
|
||||
return;
|
||||
}
|
||||
const tx = image.offsetWidth * attachmentViewer.scale > this._zoomerRef.el.offsetWidth
|
||||
? this._translate.x + this._translate.dx
|
||||
: 0;
|
||||
const ty = image.offsetHeight * attachmentViewer.scale > this._zoomerRef.el.offsetHeight
|
||||
? this._translate.y + this._translate.dy
|
||||
: 0;
|
||||
if (tx === 0) {
|
||||
this._translate.x = 0;
|
||||
}
|
||||
if (ty === 0) {
|
||||
this._translate.y = 0;
|
||||
}
|
||||
this._zoomerRef.el.style = `transform: ` +
|
||||
`translate(${tx}px, ${ty}px)`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Zoom in the image.
|
||||
*
|
||||
* @private
|
||||
* @param {Object} [param0={}]
|
||||
* @param {boolean} [param0.scroll=false]
|
||||
*/
|
||||
_zoomIn({ scroll = false } = {}) {
|
||||
this.attachmentViewer.update({
|
||||
scale: this.attachmentViewer.scale + (scroll ? SCROLL_ZOOM_STEP : ZOOM_STEP),
|
||||
});
|
||||
this._updateZoomerStyle();
|
||||
}
|
||||
|
||||
/**
|
||||
* Zoom out the image.
|
||||
*
|
||||
* @private
|
||||
* @param {Object} [param0={}]
|
||||
* @param {boolean} [param0.scroll=false]
|
||||
*/
|
||||
_zoomOut({ scroll = false } = {}) {
|
||||
if (this.attachmentViewer.scale === MIN_SCALE) {
|
||||
return;
|
||||
}
|
||||
const unflooredAdaptedScale = (
|
||||
this.attachmentViewer.scale -
|
||||
(scroll ? SCROLL_ZOOM_STEP : ZOOM_STEP)
|
||||
);
|
||||
this.attachmentViewer.update({
|
||||
scale: Math.max(MIN_SCALE, unflooredAdaptedScale),
|
||||
});
|
||||
this._updateZoomerStyle();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the zoom scale of the image.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_zoomReset() {
|
||||
this.attachmentViewer.update({ scale: 1 });
|
||||
this._updateZoomerStyle();
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Handlers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {MouseEvent} ev
|
||||
*/
|
||||
_onClickGlobal(ev) {
|
||||
if (!this.attachmentViewer.exists()) {
|
||||
return;
|
||||
}
|
||||
if (!this.attachmentViewer.isDragging) {
|
||||
return;
|
||||
}
|
||||
ev.stopPropagation();
|
||||
this._stopDragging();
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when clicking on zoom in icon.
|
||||
*
|
||||
* @private
|
||||
* @param {MouseEvent} ev
|
||||
*/
|
||||
_onClickZoomIn(ev) {
|
||||
ev.stopPropagation();
|
||||
this._zoomIn();
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when clicking on zoom out icon.
|
||||
*
|
||||
* @private
|
||||
* @param {MouseEvent} ev
|
||||
*/
|
||||
_onClickZoomOut(ev) {
|
||||
ev.stopPropagation();
|
||||
this._zoomOut();
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when clicking on reset zoom icon.
|
||||
*
|
||||
* @private
|
||||
* @param {MouseEvent} ev
|
||||
*/
|
||||
_onClickZoomReset(ev) {
|
||||
ev.stopPropagation();
|
||||
this._zoomReset();
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {KeyboardEvent} ev
|
||||
*/
|
||||
_onKeydown(ev) {
|
||||
switch (ev.key) {
|
||||
case 'ArrowRight':
|
||||
this.attachmentViewer.next();
|
||||
break;
|
||||
case 'ArrowLeft':
|
||||
this.attachmentViewer.previous();
|
||||
break;
|
||||
case 'Escape':
|
||||
this.attachmentViewer.close();
|
||||
break;
|
||||
case 'q':
|
||||
this.attachmentViewer.close();
|
||||
break;
|
||||
case 'r':
|
||||
this.attachmentViewer.rotate();
|
||||
break;
|
||||
case '+':
|
||||
this._zoomIn();
|
||||
break;
|
||||
case '-':
|
||||
this._zoomOut();
|
||||
break;
|
||||
case '0':
|
||||
this._zoomReset();
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
ev.stopPropagation();
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {DragEvent} ev
|
||||
*/
|
||||
_onMousedownImage(ev) {
|
||||
if (!this.attachmentViewer.exists()) {
|
||||
return;
|
||||
}
|
||||
if (this.attachmentViewer.isDragging) {
|
||||
return;
|
||||
}
|
||||
if (ev.button !== 0) {
|
||||
return;
|
||||
}
|
||||
ev.stopPropagation();
|
||||
this.attachmentViewer.update({ isDragging: true });
|
||||
this._dragstartX = ev.clientX;
|
||||
this._dragstartY = ev.clientY;
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {DragEvent}
|
||||
*/
|
||||
_onMousemoveView(ev) {
|
||||
if (!this.attachmentViewer.exists()) {
|
||||
return;
|
||||
}
|
||||
if (!this.attachmentViewer.isDragging) {
|
||||
return;
|
||||
}
|
||||
this._translate.dx = ev.clientX - this._dragstartX;
|
||||
this._translate.dy = ev.clientY - this._dragstartY;
|
||||
this._updateZoomerStyle();
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {Event} ev
|
||||
*/
|
||||
_onWheelImage(ev) {
|
||||
ev.stopPropagation();
|
||||
if (!this.root.el) {
|
||||
return;
|
||||
}
|
||||
if (ev.deltaY > 0) {
|
||||
this._zoomOut({ scroll: true });
|
||||
} else {
|
||||
this._zoomIn({ scroll: true });
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Object.assign(AttachmentViewer, {
|
||||
props: { record: Object },
|
||||
template: 'mail.AttachmentViewer',
|
||||
});
|
||||
|
||||
registerMessagingComponent(AttachmentViewer);
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
// ------------------------------------------------------------------
|
||||
// Layout
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
.o_AttachmentViewer {
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.o_AttachmentViewer_buttonNavigation {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.o_AttachmentViewer_buttonNavigationNextIcon {
|
||||
margin: 1px 0 0 1px; // not correctly centered for some reasons
|
||||
}
|
||||
|
||||
.o_AttachmentViewer_buttonNavigationPreviousIcon {
|
||||
margin: 1px 1px 0 0; // not correctly centered for some reasons
|
||||
}
|
||||
|
||||
.o_AttachmentViewer_header {
|
||||
height: $o-navbar-height;
|
||||
}
|
||||
|
||||
.o_AttachmentViewer_main {
|
||||
z-index: -1;
|
||||
padding: ($o-navbar-height * 1.125) 0;
|
||||
}
|
||||
|
||||
.o_AttachmentViewer_name {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.o_AttachmentViewer_zoomer {
|
||||
padding: ($o-navbar-height * 1.125) 0;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Style
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
.o_AttachmentViewer {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.o_AttachmentViewer_headerItemButton:hover {
|
||||
background-color: rgba($white, 0.1);
|
||||
color: lighten($gray-400, 15%);
|
||||
}
|
||||
|
||||
.o_AttachmentViewer_toolbarButton {
|
||||
background-color: var(--AttachmentViewer_toolbarButton-background-color, #{$o-gray-800});
|
||||
color: #fff;
|
||||
|
||||
&:hover {
|
||||
filter: brightness(1.3);
|
||||
}
|
||||
}
|
||||
|
||||
.o_AttachmentViewer_view {
|
||||
background-color: #000000;
|
||||
box-shadow: 0 0 40px #000000;
|
||||
outline: none;
|
||||
|
||||
&.o_AttachmentViewer_isText {
|
||||
background: $white;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,98 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="mail.AttachmentViewer" owl="1">
|
||||
<t t-if="attachmentViewer">
|
||||
<div class="o_AttachmentViewer flex-column align-items-center d-flex w-100 h-100" t-attf-class="{{ className }}" t-on-click="attachmentViewer.onClick" t-on-keydown="_onKeydown" tabindex="0" t-ref="root">
|
||||
<div class="o_AttachmentViewer_header d-flex w-100 bg-black-75 text-400" t-on-click="attachmentViewer.onClickHeader">
|
||||
<t t-if="attachmentViewer.attachmentViewerViewable.isViewable">
|
||||
<div class="o_AttachmentViewer_headerItem o_AttachmentViewer_icon d-flex align-items-center ms-4 me-2">
|
||||
<t t-if="attachmentViewer.attachmentViewerViewable.isImage">
|
||||
<i class="fa fa-picture-o" role="img" title="Image"/>
|
||||
</t>
|
||||
<t t-if="attachmentViewer.attachmentViewerViewable.isPdf">
|
||||
<i class="fa fa-file-text" role="img" title="PDF file"/>
|
||||
</t>
|
||||
<t t-if="attachmentViewer.attachmentViewerViewable.isText">
|
||||
<i class="fa fa-file-text" role="img" title="Text file"/>
|
||||
</t>
|
||||
<t t-if="attachmentViewer.attachmentViewerViewable.isVideo">
|
||||
<i class="fa fa-video-camera" role="img" title="Video"/>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
<div class="o_AttachmentViewer_headerItem o_AttachmentViewer_name d-flex align-items-center mx-2">
|
||||
<span class="o_AttachmentViewer_nameText text-truncate" t-esc="attachmentViewer.attachmentViewerViewable.displayName"/>
|
||||
</div>
|
||||
<div class="flex-grow-1"/>
|
||||
<div class="o_AttachmentViewer_buttonDownload o_AttachmentViewer_headerItem o_AttachmentViewer_headerItemButton d-flex align-items-center px-3 cursor-pointer" t-on-click="attachmentViewer.onClickDownload" role="button" title="Download">
|
||||
<i class="o_AttachmentViewer_headerItemButtonIcon fa fa-download fa-fw" t-att-class="{ 'o-hasLabel me-2': messaging.device.sizeClass > messaging.device.sizeClasses.MD }" role="img"/>
|
||||
<t t-if="messaging.device.sizeClass > messaging.device.sizeClasses.MD">
|
||||
<span>Download</span>
|
||||
</t>
|
||||
</div>
|
||||
<div class="o_AttachmentViewer_headerItem o_AttachmentViewer_headerItemButton o_AttachmentViewer_headerItemButtonClose d-flex align-items-center mb-0 px-3 h4 text-reset cursor-pointer" t-on-click="attachmentViewer.onClickClose" role="button" title="Close (Esc)" aria-label="Close">
|
||||
<i class="fa fa-fw fa-times" role="img"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="o_AttachmentViewer_main position-absolute top-0 bottom-0 start-0 end-0 align-items-center justify-content-center d-flex" t-att-class="{ 'o_with_img overflow-hidden': attachmentViewer.attachmentViewerViewable.isImage }" t-on-mousemove="_onMousemoveView">
|
||||
<t t-if="attachmentViewer.attachmentViewerViewable.isImage">
|
||||
<div class="o_AttachmentViewer_zoomer position-absolute align-items-center justify-content-center d-flex w-100 h-100" t-ref="zoomer">
|
||||
<t t-if="attachmentViewer.isImageLoading">
|
||||
<div class="o_AttachmentViewer_loading position-absolute">
|
||||
<i class="fa fa-3x fa-circle-o-notch fa-fw fa-spin text-white" role="img" title="Loading"/>
|
||||
</div>
|
||||
</t>
|
||||
<img class="o_AttachmentViewer_view o_AttachmentViewer_viewImage mw-100 mh-100 transition-base" t-on-click="attachmentViewer.onClickImage" t-on-mousedown="_onMousedownImage" t-on-wheel="_onWheelImage" t-on-load="attachmentViewer.onLoadImage" t-att-src="attachmentViewer.attachmentViewerViewable.imageUrl" t-att-style="attachmentViewer.imageStyle" draggable="false" alt="Viewer" t-key="'image_' + attachmentViewer.attachmentViewerViewable.localId" t-ref="image_{{ attachmentViewer.attachmentViewerViewable.localId }}"/>
|
||||
</div>
|
||||
</t>
|
||||
<t t-if="attachmentViewer.attachmentViewerViewable.isPdf">
|
||||
<iframe class="o_AttachmentViewer_view o_AttachmentViewer_viewIframe o_AttachmentViewer_viewPdf w-75 h-100 border-0" t-ref="iframeViewerPdf" t-att-class="{ 'w-100': messaging.device.isSmall }" t-att-src="attachmentViewer.attachmentViewerViewable.defaultSource"/>
|
||||
</t>
|
||||
<t t-if="attachmentViewer.attachmentViewerViewable.isText">
|
||||
<iframe class="o_AttachmentViewer_view o_AttachmentViewer_viewIframe o_AttachmentViewer_isText o_text w-75 h-100 border-0" t-att-src="attachmentViewer.attachmentViewerViewable.defaultSource"/>
|
||||
</t>
|
||||
<t t-if="attachmentViewer.attachmentViewerViewable.isUrlYoutube">
|
||||
<iframe allow="autoplay; encrypted-media" class="o_AttachmentViewer_view o_AttachmentViewer_viewIframe o_AttachmentViewer_youtube w-75 h-100 border-0" t-att-src="attachmentViewer.attachmentViewerViewable.defaultSource" height="315" width="560"/>
|
||||
</t>
|
||||
<t t-if="attachmentViewer.attachmentViewerViewable.isVideo">
|
||||
<video class="o_AttachmentViewer_view o_AttachmentViewer_viewVideo w-75 h-75" t-att-class="{ 'w-100 h-100': messaging.device.isSmall }" t-on-click="attachmentViewer.onClickVideo" controls="controls">
|
||||
<source t-att-data-type="attachmentViewer.attachmentViewerViewable.mimetype" t-att-src="attachmentViewer.attachmentViewerViewable.defaultSource"/>
|
||||
</video>
|
||||
</t>
|
||||
</div>
|
||||
<t t-if="attachmentViewer.attachmentViewerViewable.isImage">
|
||||
<div class="o_AttachmentViewer_toolbar position-absolute bottom-0 d-flex" role="toolbar">
|
||||
<div class="o_AttachmentViewer_toolbarButton p-3 rounded-0" t-on-click="_onClickZoomIn" title="Zoom In (+)" role="button">
|
||||
<i class="fa fa-fw fa-plus" role="img"/>
|
||||
</div>
|
||||
<div class="o_AttachmentViewer_toolbarButton p-3 rounded-0" t-att-class="{ 'o_disabled opacity-50': attachmentViewer.scale === 1 }" t-on-click="_onClickZoomReset" role="button" title="Reset Zoom (0)">
|
||||
<i class="fa fa-fw fa-search" role="img"/>
|
||||
</div>
|
||||
<div class="o_AttachmentViewer_toolbarButton p-3 rounded-0" t-att-class="{ 'o_disabled opacity-50': attachmentViewer.scale === MIN_SCALE }" t-on-click="_onClickZoomOut" title="Zoom Out (-)" role="button">
|
||||
<i class="fa fa-fw fa-minus" role="img"/>
|
||||
</div>
|
||||
<div class="o_AttachmentViewer_toolbarButton p-3 rounded-0" t-on-click="attachmentViewer.onClickRotate" title="Rotate (r)" role="button">
|
||||
<i class="fa fa-fw fa-repeat" role="img"/>
|
||||
</div>
|
||||
<div class="o_AttachmentViewer_toolbarButton p-3 rounded-0" t-on-click="attachmentViewer.onClickPrint" title="Print" role="button">
|
||||
<i class="fa fa-fw fa-print" role="img"/>
|
||||
</div>
|
||||
<div class="o_AttachmentViewer_buttonDownload o_AttachmentViewer_toolbarButton p-3 rounded-0 cursor-pointer" t-on-click="attachmentViewer.onClickDownload" title="Download" role="button">
|
||||
<i class="fa fa-download fa-fw" role="img"/>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
<t t-if="attachmentViewer.attachmentViewerViewables.length > 1">
|
||||
<div class="o_AttachmentViewer_buttonNavigation o_AttachmentViewer_buttonNavigationPrevious o_AttachmentViewer_buttonNavigationPreviousIcon position-absolute top-0 bottom-0 start-0 align-items-center justify-content-center d-flex my-auto ms-3 rounded-circle bg-dark text-white" t-on-click="attachmentViewer.onClickPrevious" title="Previous (Left-Arrow)" role="button">
|
||||
<span class="fa fa-chevron-left" role="img"/>
|
||||
</div>
|
||||
<div class="o_AttachmentViewer_buttonNavigation o_AttachmentViewer_buttonNavigationNext o_AttachmentViewer_buttonNavigationNextIcon position-absolute top-0 bottom-0 end-0 align-items-center justify-content-center d-flex my-auto me-3 rounded-circle bg-dark text-white" t-on-click="attachmentViewer.onClickNext" title="Next (Right-Arrow)" role="button">
|
||||
<span class="fa fa-chevron-right" role="img"/>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { useComponentToModel } from '@mail/component_hooks/use_component_to_model';
|
||||
import { registerMessagingComponent } from '@mail/utils/messaging_component';
|
||||
|
||||
const { Component, onMounted, onWillUnmount } = owl;
|
||||
|
||||
export class AutocompleteInputView extends Component {
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
setup() {
|
||||
super.setup();
|
||||
useComponentToModel({ fieldName: 'component' });
|
||||
onMounted(() => this._mounted());
|
||||
onWillUnmount(() => this._willUnmount());
|
||||
}
|
||||
|
||||
_mounted() {
|
||||
if (!this.root.el) {
|
||||
return;
|
||||
}
|
||||
if (this.autocompleteInputView.isFocusOnMount) {
|
||||
this.root.el.focus();
|
||||
}
|
||||
|
||||
const args = {
|
||||
autoFocus: true,
|
||||
select: (ev, ui) => {
|
||||
if (this.autocompleteInputView) {
|
||||
this.autocompleteInputView.onSelect(ev, ui);
|
||||
}
|
||||
},
|
||||
source: (req, res) => {
|
||||
if (this.autocompleteInputView) {
|
||||
this.autocompleteInputView.onSource(req, res);
|
||||
}
|
||||
},
|
||||
html: this.autocompleteInputView.isHtml,
|
||||
};
|
||||
|
||||
if (this.autocompleteInputView.customClass) {
|
||||
args.classes = { 'ui-autocomplete': this.autocompleteInputView.customClass };
|
||||
}
|
||||
|
||||
const autoCompleteElem = $(this.root.el).autocomplete(args);
|
||||
// Resize the autocomplete dropdown options to handle the long strings
|
||||
// By setting the width of dropdown based on the width of the input element.
|
||||
autoCompleteElem.data('ui-autocomplete')._resizeMenu = function () {
|
||||
const ul = this.menu.element;
|
||||
ul.outerWidth(this.element.outerWidth());
|
||||
};
|
||||
}
|
||||
|
||||
_willUnmount() {
|
||||
if (!this.root.el) {
|
||||
return;
|
||||
}
|
||||
$(this.root.el).autocomplete('destroy');
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {AutocompleteInputView}
|
||||
*/
|
||||
get autocompleteInputView() {
|
||||
return this.props.record;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Object.assign(AutocompleteInputView, {
|
||||
props: { record: Object },
|
||||
template: 'mail.AutocompleteInputView',
|
||||
});
|
||||
|
||||
registerMessagingComponent(AutocompleteInputView);
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="mail.AutocompleteInputView" owl="1">
|
||||
<input class="o_AutocompleteInputView form-control" t-attf-class="{{ className }}" t-on-blur="autocompleteInputView.onBlur" t-on-focusin="autocompleteInputView.onFocusin" t-on-keydown="autocompleteInputView.onKeydown" t-att-placeholder="autocompleteInputView.placeholder" t-ref="root"/>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
// = Call Action List View
|
||||
// ============================================================================
|
||||
// No CSS hacks, variables overrides only
|
||||
|
||||
.o_CallActionList_button:not(.btn-danger) {
|
||||
--CallActionList_button-background-color: #{$o-gray-200};
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { useRefToModel } from '@mail/component_hooks/use_ref_to_model';
|
||||
import { registerMessagingComponent } from '@mail/utils/messaging_component';
|
||||
|
||||
const { Component } = owl;
|
||||
|
||||
export class CallActionList extends Component {
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
setup() {
|
||||
super.setup();
|
||||
useRefToModel({ fieldName: 'moreButtonRef', refName: 'moreButton' });
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {CallActionListView}
|
||||
*/
|
||||
get callActionListView() {
|
||||
return this.props.record;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Object.assign(CallActionList, {
|
||||
props: { record: Object },
|
||||
template: 'mail.CallActionList',
|
||||
});
|
||||
|
||||
registerMessagingComponent(CallActionList);
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
.o_CallActionList_button:not(.btn-danger) {
|
||||
background-color: var(--CallActionList_button-background-color, #{$o-gray-800});
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
|
@ -0,0 +1,144 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="mail.CallActionList" owl="1">
|
||||
<t t-if="callActionListView">
|
||||
<div class="o_CallActionList d-flex justify-content-between" t-attf-class="{{ className }}" t-ref="root">
|
||||
<div class="o_CallActionList_buttons d-flex align-items-center flex-wrap">
|
||||
<t t-if="callActionListView.thread.rtc and messaging.rtc.currentRtcSession">
|
||||
<button class="o_CallActionList_button btn d-flex m-1 border-0 rounded-circle shadow-none opacity-100 opacity-75-hover"
|
||||
t-att-class="{ 'o-isActive': !messaging.rtc.currentRtcSession.isMute, 'o-isSmall p-2': callActionListView.isSmall, 'p-3': !callActionListView.isSmall }"
|
||||
t-att-aria-label="callActionListView.microphoneButtonTitle"
|
||||
t-att-title="callActionListView.microphoneButtonTitle"
|
||||
t-on-click="callActionListView.onClickMicrophone">
|
||||
<div class="o_CallActionList_buttonIconWrapper fa-stack" t-att-class="{ 'o-isSmall': callActionListView.isSmall }">
|
||||
<i class="fa fa-stack-1x" t-att-class="{
|
||||
'fa-lg': !callActionListView.isSmall,
|
||||
'fa-microphone': !messaging.rtc.currentRtcSession.isMute,
|
||||
'fa-microphone-slash': messaging.rtc.currentRtcSession.isMute,
|
||||
'text-danger': messaging.rtc.currentRtcSession.isMute,
|
||||
}"/>
|
||||
</div>
|
||||
</button>
|
||||
<button class="o_CallActionList_button btn d-flex m-1 border-0 rounded-circle shadow-none opacity-100 opacity-75-hover"
|
||||
t-att-class="{ 'o-isActive': !messaging.rtc.currentRtcSession.isDeaf, 'o-isSmall p-2': callActionListView.isSmall, 'p-3': !callActionListView.isSmall }"
|
||||
t-att-aria-label="callActionListView.headphoneButtonTitle"
|
||||
t-att-title="callActionListView.headphoneButtonTitle"
|
||||
t-on-click="callActionListView.onClickDeafen">
|
||||
<div class="o_CallActionList_buttonIconWrapper fa-stack" t-att-class="{ 'o-isSmall': callActionListView.isSmall }">
|
||||
<i class="fa fa-stack-1x" t-att-class="{
|
||||
'fa-lg': !callActionListView.isSmall,
|
||||
'fa-headphones': !messaging.rtc.currentRtcSession.isDeaf,
|
||||
'fa-deaf': messaging.rtc.currentRtcSession.isDeaf,
|
||||
'text-danger': messaging.rtc.currentRtcSession.isDeaf,
|
||||
}"/>
|
||||
</div>
|
||||
</button>
|
||||
<button class="o_CallActionList_button o_CallActionList_videoButton btn d-flex m-1 border-0 rounded-circle shadow-none opacity-100 opacity-75-hover"
|
||||
t-att-class="{
|
||||
'o-isActive': messaging.rtc.sendUserVideo,
|
||||
'o-isSmall p-2': callActionListView.isSmall,
|
||||
'p-3': !callActionListView.isSmall,
|
||||
}"
|
||||
t-att-aria-label="callActionListView.cameraButtonTitle"
|
||||
t-att-title="callActionListView.cameraButtonTitle"
|
||||
t-on-click="callActionListView.onClickCamera">
|
||||
<div class="o_CallActionList_buttonIconWrapper fa-stack" t-att-class="{ 'o-isSmall': callActionListView.isSmall }">
|
||||
<i class="fa fa-video-camera fa-stack-1x" t-att-class="{ 'fa-lg': !callActionListView.isSmall, 'text-success': messaging.rtc.sendUserVideo }"/>
|
||||
</div>
|
||||
</button>
|
||||
<t t-if="!messaging.device.isMobileDevice">
|
||||
<button class="o_CallActionList_button o_CallActionList_videoButton btn d-flex m-1 border-0 rounded-circle shadow-none opacity-100 opacity-75-hover"
|
||||
t-att-class="{
|
||||
'o-isActive': messaging.rtc.sendDisplay,
|
||||
'o-isSmall p-2': callActionListView.isSmall,
|
||||
'p-3': !callActionListView.isSmall,
|
||||
}"
|
||||
t-att-aria-label="callActionListView.screenSharingButtonTitle"
|
||||
t-att-title="callActionListView.screenSharingButtonTitle"
|
||||
t-on-click="callActionListView.onClickScreen">
|
||||
<div class="o_CallActionList_buttonIconWrapper fa-stack" t-att-class="{ 'o-isSmall': callActionListView.isSmall }">
|
||||
<i class="fa fa-desktop fa-stack-1x" t-att-class="{ 'fa-lg': !callActionListView.isSmall, 'text-success': messaging.rtc.sendDisplay }"/>
|
||||
</div>
|
||||
</button>
|
||||
</t>
|
||||
<t t-if="!callActionListView.callView.isFullScreen">
|
||||
<button class="o_CallActionList_button btn d-flex m-1 border-0 rounded-circle shadow-none opacity-100 opacity-75-hover"
|
||||
aria-label="Activate Full Screen"
|
||||
title="Activate Full Screen"
|
||||
t-att-class="{ 'o-isSmall p-2': callActionListView.isSmall, 'p-3': !callActionListView.isSmall }"
|
||||
t-on-click="callActionListView.callView.activateFullScreen"
|
||||
>
|
||||
<div class="o_CallActionList_buttonIconWrapper fa-stack" t-att-class="{ 'o-isSmall': callActionListView.isSmall }">
|
||||
<i class="fa fa-arrows-alt fa-stack-1x" t-att-class="{ 'fa-lg': !callActionListView.isSmall }"/>
|
||||
</div>
|
||||
</button>
|
||||
</t>
|
||||
<t t-if="callActionListView.callView.isFullScreen">
|
||||
<button class="o_CallActionList_button btn d-flex m-1 border-0 rounded-circle shadow-none opacity-100 opacity-75-hover"
|
||||
aria-label="Deactivate Full Screen"
|
||||
title="Deactivate Full Screen"
|
||||
t-att-class="{ 'o-isSmall p-2': callActionListView.isSmall, 'p-3': !callActionListView.isSmall }"
|
||||
t-on-click="callActionListView.callView.deactivateFullScreen"
|
||||
>
|
||||
<div class="o_CallActionList_buttonIconWrapper fa-stack" t-att-class="{ 'o-isSmall': callActionListView.isSmall }">
|
||||
<i class="fa fa-compress fa-stack-1x" t-att-class="{ 'fa-lg': !callActionListView.isSmall }"/>
|
||||
</div>
|
||||
</button>
|
||||
</t>
|
||||
<t t-if="messaging.modelManager.isDebug">
|
||||
<button class="o_CallActionList_button btn d-flex m-1 border-0 rounded-circle shadow-none opacity-100 opacity-75-hover"
|
||||
aria-label="More"
|
||||
title="More"
|
||||
t-att-class="{ 'o-isSmall p-2': callActionListView.isSmall, 'p-3': !callActionListView.isSmall }"
|
||||
t-on-click="callActionListView.onClickMore"
|
||||
t-ref="moreButton"
|
||||
>
|
||||
<div class="o_CallActionList_buttonIconWrapper fa-stack" t-att-class="{ 'o-isSmall': callActionListView.isSmall }">
|
||||
<i class="fa fa-ellipsis-h fa-stack-1x" t-att-class="{ 'fa-lg': !callActionListView.isSmall }"/>
|
||||
</div>
|
||||
</button>
|
||||
</t>
|
||||
</t>
|
||||
<t t-if="!callActionListView.thread">
|
||||
<button class="o_CallActionList_button o_CallActionList_callToggle btn btn-success d-flex m-1 border-0 rounded-circle shadow-none"
|
||||
t-att-class="{ 'o-isSmall p-2': callActionListView.isSmall, 'p-3': !callActionListView.isSmall }"
|
||||
aria-label="Join Video Call"
|
||||
title="Join Video Call"
|
||||
t-att-disabled="callActionListView.thread.hasPendingRtcRequest"
|
||||
t-on-click="callActionListView.onClickToggleVideoCall">
|
||||
<div class="o_CallActionList_buttonIconWrapper fa-stack" t-att-class="{ 'o-isSmall': callActionListView.isSmall }">
|
||||
<i class="fa fa-video-camera fa-stack-1x" t-att-class="{ 'fa-lg': !callActionListView.isSmall }"/>
|
||||
</div>
|
||||
</button>
|
||||
</t>
|
||||
<t t-if="callActionListView.thread">
|
||||
<t t-if="callActionListView.thread.rtcInvitingSession and !callActionListView.thread.rtc">
|
||||
<button class="o_CallActionList_button o_CallActionList_callToggle o-isActive btn btn-danger d-flex m-1 border-0 rounded-circle shadow-none"
|
||||
t-att-class="{ 'o-isSmall p-2': callActionListView.isSmall, 'p-3': !callActionListView.isSmall }"
|
||||
aria-label="Reject"
|
||||
title="Reject"
|
||||
t-att-disabled="callActionListView.thread.hasPendingRtcRequest"
|
||||
t-on-click="callActionListView.onClickRejectCall">
|
||||
<div class="o_CallActionList_buttonIconWrapper fa-stack" t-att-class="{ 'o-isSmall': callActionListView.isSmall }">
|
||||
<i class="fa fa-phone fa-stack-1x" t-att-class="{ 'fa-lg': !callActionListView.isSmall }"/>
|
||||
</div>
|
||||
</button>
|
||||
</t>
|
||||
<button class="o_CallActionList_button o_CallActionList_callToggle btn d-flex m-1 border-0 rounded-circle shadow-none"
|
||||
t-att-aria-label="callActionListView.callButtonTitle"
|
||||
t-att-class="{ 'o-isActive btn-danger': !!callActionListView.thread.rtc, 'o-isSmall p-2': callActionListView.isSmall, 'p-3': !callActionListView.isSmall, 'btn-success': !callActionListView.thread.rtc }"
|
||||
t-att-disabled="callActionListView.thread.hasPendingRtcRequest"
|
||||
t-att-title="callActionListView.callButtonTitle"
|
||||
t-on-click="callActionListView.onClickToggleAudioCall">
|
||||
<div class="o_CallActionList_buttonIconWrapper fa-stack" t-att-class="{ 'o-isSmall': callActionListView.isSmall }">
|
||||
<i class="fa fa-phone fa-stack-1x" t-att-class="{ 'fa-lg': !callActionListView.isSmall }"/>
|
||||
</div>
|
||||
</button>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { useRefToModel } from '@mail/component_hooks/use_ref_to_model';
|
||||
import { registerMessagingComponent } from '@mail/utils/messaging_component';
|
||||
|
||||
const { Component } = owl;
|
||||
|
||||
export class CallDemoView extends Component {
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
setup() {
|
||||
super.setup();
|
||||
useRefToModel({ fieldName: 'audioRef', refName: 'audio' });
|
||||
useRefToModel({ fieldName: 'videoRef', refName: 'video' });
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {CallDemoView}
|
||||
*/
|
||||
get callDemoView() {
|
||||
return this.props.record;
|
||||
}
|
||||
}
|
||||
|
||||
Object.assign(CallDemoView, {
|
||||
props: { record: Object },
|
||||
template: 'mail.CallDemoView',
|
||||
});
|
||||
|
||||
registerMessagingComponent(CallDemoView);
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
.o_CallDemoView {
|
||||
max-width: max-content;
|
||||
}
|
||||
|
||||
.o_CallDemoView_mediaDevicesStatus {
|
||||
@include o-position-absolute($bottom: 50%);
|
||||
}
|
||||
|
||||
.o_CallDemoView_buttonsContainer {
|
||||
@include o-position-absolute($bottom: 0);
|
||||
}
|
||||
|
||||
.o_CallDemoView_button {
|
||||
height: 4rem;
|
||||
width: 4rem;
|
||||
font-size: 1.5em;
|
||||
line-height: 4rem;
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="mail.CallDemoView" owl="1">
|
||||
<t t-if="callDemoView">
|
||||
<div class="o_CallDemoView position-relative d-flex justify-content-center" t-attf-class="{{ className }}" t-ref="root">
|
||||
<video class="o_CallDemoView_videoDisplay shadow rounded bg-dark" height="480" width="640" autoplay="" t-ref="video"/>
|
||||
<p t-if="callDemoView.doesBrowserSupportMediaDevices and !callDemoView.isVideoEnabled" class="o_CallDemoView_mediaDevicesStatus position-absolute text-light">
|
||||
Camera is off
|
||||
</p>
|
||||
<p t-if="!callDemoView.doesBrowserSupportMediaDevices" class="o_CallDemoView_mediaDevicesStatus text-light">
|
||||
Your browser does not support videoconference
|
||||
</p>
|
||||
<div class="o_CallDemoView_buttonsContainer">
|
||||
<button t-if="!callDemoView.isMicrophoneEnabled" class="o_CallDemoView_enableMicrophoneButton o_CallDemoView_button btn btn-danger btn-lg rounded-circle p-0 m-3 fa fa-microphone-slash" t-on-click="callDemoView.onClickEnableMicrophoneButton"/>
|
||||
<button t-if="callDemoView.isMicrophoneEnabled" class="o_CallDemoView_disableMicrophoneButton o_CallDemoView_button btn btn-dark btn-lg p-0 m-3 rounded-circle border-light fa fa-microphone" t-on-click="callDemoView.onClickDisableMicrophoneButton"/>
|
||||
<button t-if="!callDemoView.isVideoEnabled" class="o_CallDemoView_enableVideoButton o_CallDemoView_button btn btn-danger btn-lg p-0 m-3 rounded-circle fa fa-eye-slash" t-on-click="callDemoView.onClickEnableVideoButton"/>
|
||||
<button t-if="callDemoView.isVideoEnabled" class="o_CallDemoView_disableVideoButton o_CallDemoView_button btn btn-dark btn-lg p-0 m-3 rounded-circle border-light fa fa-video-camera" t-on-click="callDemoView.onClickDisableVideoButton"/>
|
||||
</div>
|
||||
<audio class="o_CallDemoView_audioPlayer" autoplay="" t-ref="audio"/>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { registerMessagingComponent } from '@mail/utils/messaging_component';
|
||||
|
||||
const { Component } = owl;
|
||||
|
||||
export class CallInviteRequestPopup extends Component {
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Getters / Setters
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @returns {CallInviteRequestPopup}
|
||||
*/
|
||||
get callInviteRequestPopup() {
|
||||
return this.props.record;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Object.assign(CallInviteRequestPopup, {
|
||||
props: { record: Object },
|
||||
template: 'mail.CallInviteRequestPopup',
|
||||
});
|
||||
|
||||
registerMessagingComponent(CallInviteRequestPopup);
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
// ------------------------------------------------------------------
|
||||
// Layout
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
.o_CallInviteRequestPopup_partnerInfo {
|
||||
width: 130px;
|
||||
}
|
||||
|
||||
.o_CallInviteRequestPopup_partnerInfoImage {
|
||||
width: 70%;
|
||||
height: 70%;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Style
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
.o_CallInviteRequestPopup_buttonListButton {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.o_CallInviteRequestPopup_partnerInfoImage {
|
||||
border: 3px solid gray;
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="mail.CallInviteRequestPopup" owl="1">
|
||||
<t t-if="callInviteRequestPopup">
|
||||
<div class="o_CallInviteRequestPopup d-flex flex-column m-2 p-5 border border-dark rounded-1 bg-900" t-attf-class="{{ className }}" t-ref="root">
|
||||
<t t-if="callInviteRequestPopup.thread.rtcInvitingSession">
|
||||
<div class="o_CallInviteRequestPopup_partnerInfo d-flex flex-column justify-content-around align-items-center text-nowrap">
|
||||
<img class="o_CallInviteRequestPopup_partnerInfoImage mb-2 rounded-circle cursor-pointer"
|
||||
t-att-src="callInviteRequestPopup.thread.rtcInvitingSession.channelMember.avatarUrl"
|
||||
t-on-click="callInviteRequestPopup.onClickAvatar"
|
||||
alt="Avatar"/>
|
||||
<span class="o_CallInviteRequestPopup_partnerInfoName w-100 fw-bolder text-truncate text-center overflow-hidden" t-esc="callInviteRequestPopup.thread.rtcInvitingSession.channelMember.persona.name"/>
|
||||
<span class="o_CallInviteRequestPopup_partnerInfoText fst-italic opacity-75">Incoming Call...</span>
|
||||
</div>
|
||||
</t>
|
||||
<div class="o_CallInviteRequestPopup_buttonList d-flex justify-content-around align-items-center w-100 mt-4">
|
||||
<button class="o_CallInviteRequestPopup_buttonListButton o_CallInviteRequestPopup_buttonListRefuse p-2 rounded-circle border-0 bg-danger"
|
||||
aria-label="Refuse"
|
||||
title="Refuse"
|
||||
t-on-click="callInviteRequestPopup.onClickRefuse">
|
||||
<i class="o_CallInviteRequestPopup_buttonListButtonIcon fa fa-lg fa-times m-3"/>
|
||||
</button>
|
||||
<button class="o_CallInviteRequestPopup_buttonListButton o_CallInviteRequestPopup_buttonListAccept p-2 rounded-circle border-0 bg-success"
|
||||
aria-label="Accept"
|
||||
title="Accept"
|
||||
t-on-click="callInviteRequestPopup.onClickAccept">
|
||||
<i class="o_CallInviteRequestPopup_buttonListButtonIcon fa fa-lg fa-phone m-3"/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { registerMessagingComponent } from '@mail/utils/messaging_component';
|
||||
|
||||
const { Component } = owl;
|
||||
|
||||
export class CallInviteRequestPopupList extends Component {}
|
||||
|
||||
Object.assign(CallInviteRequestPopupList, {
|
||||
props: {},
|
||||
template: 'mail.CallInviteRequestPopupList',
|
||||
});
|
||||
|
||||
registerMessagingComponent(CallInviteRequestPopupList);
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
// ------------------------------------------------------------------
|
||||
// Layout
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
.o_CallInviteRequestPopupList {
|
||||
z-index: $zindex-modal;
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="mail.CallInviteRequestPopupList" owl="1">
|
||||
<div class="o_CallInviteRequestPopupList position-absolute top-0 end-0 d-flex flex-column p-2" t-attf-class="{{ className }}" t-ref="root">
|
||||
<t t-if="messaging.ringingThreads">
|
||||
<t t-foreach="messaging.callInviteRequestPopups" t-as="callInviteRequestPopup" t-key="callInviteRequestPopup.localId">
|
||||
<CallInviteRequestPopup record="callInviteRequestPopup"/>
|
||||
</t>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { useComponentToModel } from '@mail/component_hooks/use_component_to_model';
|
||||
import { useUpdateToModel } from '@mail/component_hooks/use_update_to_model';
|
||||
import { useRefToModel } from '@mail/component_hooks/use_ref_to_model';
|
||||
import { registerMessagingComponent } from '@mail/utils/messaging_component';
|
||||
|
||||
const { Component, onMounted, onWillUnmount } = owl;
|
||||
|
||||
export class CallMainView extends Component {
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
setup() {
|
||||
super.setup();
|
||||
useComponentToModel({ fieldName: 'component' });
|
||||
useRefToModel({ fieldName: 'tileContainerRef', refName: 'tileContainer', });
|
||||
useUpdateToModel({ methodName: 'onComponentUpdate' });
|
||||
onMounted(() => {
|
||||
this.resizeObserver = new ResizeObserver(() => this.callMainView.onResize());
|
||||
this.resizeObserver.observe(this.root.el);
|
||||
});
|
||||
onWillUnmount(() => this.resizeObserver.disconnect());
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Getters / Setters
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @returns {CallMainView}
|
||||
*/
|
||||
get callMainView() {
|
||||
return this.props.record;
|
||||
}
|
||||
}
|
||||
|
||||
Object.assign(CallMainView, {
|
||||
props: { record: Object },
|
||||
template: 'mail.CallMainView',
|
||||
});
|
||||
|
||||
registerMessagingComponent(CallMainView);
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
// ------------------------------------------------------------------
|
||||
// Layout
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
.o_CallMainView {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.o_CallMainView_gridParticipantCard {
|
||||
width: var(--width);
|
||||
height: var(--height);
|
||||
min-width: var(--width);
|
||||
min-height: var(--height);
|
||||
aspect-ratio: 16/9;
|
||||
}
|
||||
|
||||
.o_CallMainView_sidebarButton {
|
||||
font-size: 1.2rem;
|
||||
padding: map-get($spacers, 2);
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
right: 0;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Style
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
.o_CallMainView_sidebarButton {
|
||||
color: white;
|
||||
border-radius: 10px 0px 0px 10px;
|
||||
border-right: 0px;
|
||||
background-color: lighten(black, 10%);
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="mail.CallMainView" owl="1">
|
||||
<t t-if="callMainView">
|
||||
<div class="o_CallMainView d-flex flex-grow-1 flex-column align-items-center justify-content-center position-relative bg-black-50" t-ref="root" t-on-mouseleave="callMainView.onMouseleave">
|
||||
<div
|
||||
class="o_CallMainView_grid d-flex align-items-center overflow-hidden h-100 w-100 flex-wrap justify-content-center"
|
||||
t-attf-style="--height:{{callMainView.tileHeight}}px; --width:{{callMainView.tileWidth}}px;"
|
||||
t-on-click="callMainView.onClick"
|
||||
t-on-mousemove="callMainView.onMouseMove"
|
||||
t-ref="tileContainer"
|
||||
>
|
||||
<t t-foreach="callMainView.mainTiles" t-as="tile" t-key="'grid_tile_'+tile.localId">
|
||||
<CallParticipantCard
|
||||
className="'o_CallMainView_gridParticipantCard'"
|
||||
record="tile.participantCard"
|
||||
/>
|
||||
</t>
|
||||
</div>
|
||||
|
||||
<!-- Buttons -->
|
||||
<t t-if="callMainView.hasSidebarButton">
|
||||
<i t-if="callMainView.callView.isSidebarOpen" class="o_CallMainView_sidebarButton cursor-pointer position-absolute fa fa-arrow-right" title="Hide sidebar" t-on-click="callMainView.onClickHideSidebar"/>
|
||||
<i t-else="" class="o_CallMainView_sidebarButton cursor-pointer position-absolute fa fa-arrow-left" title="Show sidebar" t-on-click="callMainView.onClickShowSidebar"/>
|
||||
</t>
|
||||
<t t-if="callMainView.showOverlay or !callMainView.isControllerFloating">
|
||||
<div class="o_CallMainView_controls d-flex justify-content-center w-100 pb-1" t-att-class="{ 'o-isFloating position-absolute bottom-0 pb-3': callMainView.isControllerFloating }">
|
||||
<div class="o_CallMainView_controlsOverlayContainer" t-on-mousemove="callMainView.onMouseMoveOverlay">
|
||||
<CallActionList record="callMainView.callActionListView"/>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { useComponentToModel } from '@mail/component_hooks/use_component_to_model';
|
||||
import { registerMessagingComponent } from '@mail/utils/messaging_component';
|
||||
import { LegacyComponent } from "@web/legacy/legacy_component";
|
||||
|
||||
export class CallOptionMenu extends LegacyComponent {
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
setup() {
|
||||
super.setup();
|
||||
useComponentToModel({ fieldName: 'component' });
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {CallOptionMenu}
|
||||
*/
|
||||
get callOptionMenu() {
|
||||
return this.props.record;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Object.assign(CallOptionMenu, {
|
||||
props: { record: Object },
|
||||
template: 'mail.CallOptionMenu',
|
||||
});
|
||||
|
||||
registerMessagingComponent(CallOptionMenu);
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
// Style
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
.o_CallOptionMenu_button {
|
||||
&:hover {
|
||||
background-color: $gray-100;
|
||||
box-shadow: 0px 0px 1px 1px $gray-300 inset;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: $gray-200;
|
||||
box-shadow: 0px 0px 1px 1px $gray-400 inset;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="mail.CallOptionMenu" owl="1">
|
||||
<t t-if="callOptionMenu">
|
||||
<div class="o_CallOptionMenu d-flex flex-column p-3" t-attf-class="{{ className }}" t-ref="root">
|
||||
<button class="o_CallOptionMenu_button btn d-flex align-items-center border-0 rounded text-800 fw-normal" t-on-click="callOptionMenu.onClickDownloadLogs">
|
||||
<i class="o_CallOptionMenu_buttonIcon fa fa-lg fa-file-text-o m-1 p-1"/>
|
||||
<span class="o_CallOptionMenu_buttonText text-truncate">Download logs</span>
|
||||
</button>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
// = Call Participant Card View
|
||||
// ============================================================================
|
||||
// No CSS hacks, variables overrides only
|
||||
|
||||
.o_CallParticipantCard_avatarFrame {
|
||||
--CallParticipantCard_avatarFrame-background-color: #{$o-gray-100};
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { useRefToModel } from '@mail/component_hooks/use_ref_to_model';
|
||||
import { registerMessagingComponent } from '@mail/utils/messaging_component';
|
||||
|
||||
const { Component } = owl;
|
||||
|
||||
export class CallParticipantCard extends Component {
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
setup() {
|
||||
super.setup();
|
||||
useRefToModel({ fieldName: 'volumeMenuAnchorRef', refName: 'volumeMenuAnchor' });
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Getters / Setters
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @returns {Thread|undefined}
|
||||
*/
|
||||
get callParticipantCard() {
|
||||
return this.props.record;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Object.assign(CallParticipantCard, {
|
||||
props: { record: Object },
|
||||
template: 'mail.CallParticipantCard',
|
||||
});
|
||||
|
||||
registerMessagingComponent(CallParticipantCard);
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
// ------------------------------------------------------------------
|
||||
// Layout
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
.o_CallParticipantCard {
|
||||
aspect-ratio: 16/9;
|
||||
}
|
||||
|
||||
.o_CallParticipantCard_avatarImage {
|
||||
max-height: #{"min(100%, 100px)"}; // interpolated as not supported by Sass
|
||||
max-width: #{"min(100%, 100px)"};
|
||||
aspect-ratio: 1;
|
||||
border: solid $gray-500;
|
||||
|
||||
&.o-isTalking {
|
||||
border: solid darken($o-enterprise-primary-color, 5%);
|
||||
}
|
||||
|
||||
&.o-isInvitation:not(:hover) {
|
||||
animation: o_CallParticipantCard_avatarImag_borderPulse 3s linear infinite;
|
||||
}
|
||||
|
||||
&.o-isInvitation:hover {
|
||||
border: solid map-get($theme-colors, 'danger');
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes o_CallParticipantCard_avatarImag_borderPulse {
|
||||
0% { border: solid white }
|
||||
20% { border: solid $gray-600 }
|
||||
35% { border: solid $gray-100 }
|
||||
50% { border: solid $gray-600 }
|
||||
70% { border: solid $gray-100 }
|
||||
85% { border: solid $gray-700 }
|
||||
}
|
||||
|
||||
.o_CallParticipantCard_overlay {
|
||||
pointer-events: none;
|
||||
margin: Min(5%, map-get($spacers, 2));
|
||||
}
|
||||
|
||||
.o_CallParticipantCard_overlayBottom {
|
||||
max-width: 50%;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Style
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
.o_CallParticipantCard {
|
||||
&.o-isTalking {
|
||||
box-shadow: inset 0 0 0 map-get($spacers, 1) darken($o-enterprise-primary-color, 5%);
|
||||
}
|
||||
}
|
||||
|
||||
.o_CallParticipantCard_liveIndicator {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.o_CallParticipantCard_avatarFrame:not(.o-isMinimized) {
|
||||
background-color: var(--CallParticipantCard_avatarFrame-background-color, #{$o-gray-700});
|
||||
}
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="mail.CallParticipantCard" owl="1">
|
||||
<t t-if="callParticipantCard">
|
||||
<div class="o_CallParticipantCard position-relative cursor-pointer d-flex flex-column align-items-center justify-content-center mh-100 mw-100 p-1 rounded-1"
|
||||
t-att-class="{
|
||||
'o-isTalking': !callParticipantCard.isMinimized and callParticipantCard.isTalking,
|
||||
'o-isInvitation opacity-50': !callParticipantCard.rtcSession,
|
||||
}"
|
||||
t-att-title="callParticipantCard.channelMember.persona.name"
|
||||
t-att-aria-label="callParticipantCard.channelMember.persona.name"
|
||||
t-attf-class="{{ className }}"
|
||||
t-on-click="callParticipantCard.onClick"
|
||||
t-on-contextmenu="callParticipantCard.onContextMenu"
|
||||
t-ref="root"
|
||||
>
|
||||
<!-- card -->
|
||||
<t t-if="callParticipantCard.callParticipantVideoView">
|
||||
<CallParticipantVideo record="callParticipantCard.callParticipantVideoView"/>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<div class="o_CallParticipantCard_avatarFrame d-flex align-items-center justify-content-center h-100 w-100 rounded-1" t-att-class="{ 'o-isMinimized': callParticipantCard.isMinimized }" draggable="false">
|
||||
<img alt="Avatar"
|
||||
t-att-class="{
|
||||
'o-isTalking': callParticipantCard.isTalking,
|
||||
'o-isInvitation': !callParticipantCard.rtcSession,
|
||||
}"
|
||||
class="o_CallParticipantCard_avatarImage h-100 rounded-circle border-5 o_object_fit_cover"
|
||||
t-att-src="callParticipantCard.channelMember.avatarUrl"
|
||||
draggable="false"
|
||||
/>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<t t-if="callParticipantCard.rtcSession">
|
||||
<!-- overlay -->
|
||||
<span class="o_CallParticipantCard_overlay o_CallParticipantCard_overlayBottom position-absolute bottom-0 start-0 d-flex overflow-hidden">
|
||||
<t t-if="!callParticipantCard.isMinimized">
|
||||
<span class="o_CallParticipantCard_name p-1 rounded-1 bg-black-75 text-truncate" t-esc="callParticipantCard.channelMember.persona.name"/>
|
||||
</t>
|
||||
<t t-if="callParticipantCard.rtcSession.isScreenSharingOn and callParticipantCard.isMinimized and !callParticipantCard.rtcSession.channel.rtc">
|
||||
<small class="o_CallParticipantCard_liveIndicator o-isMinimized rounded-pill text-bg-danger d-flex align-items-center fw-bolder" title="live" aria-label="live">
|
||||
LIVE
|
||||
</small>
|
||||
</t>
|
||||
</span>
|
||||
<div class="o_CallParticipantCard_overlay o_CallParticipantCard_overlayTop position-absolute top-0 end-0 d-flex flex-row-reverse">
|
||||
<t t-if="callParticipantCard.rtcSession.isSelfMuted and !callParticipantCard.rtcSession.isDeaf">
|
||||
<span class="o_CallParticipantCard_overlayTopElement d-flex flex-column justify-content-center me-1 rounded-circle bg-900" t-att-class="{'o-isMinimized p-1': callParticipantCard.isMinimized, 'p-2': !callParticipantCard.isMinimized }" title="muted" aria-label="muted">
|
||||
<i class="fa fa-microphone-slash"/>
|
||||
</span>
|
||||
</t>
|
||||
<t t-if="callParticipantCard.rtcSession.isDeaf">
|
||||
<span class="o_CallParticipantCard_overlayTopElement d-flex flex-column justify-content-center me-1 rounded-circle bg-900" t-att-class="{'o-isMinimized p-1': callParticipantCard.isMinimized, 'p-2': !callParticipantCard.isMinimized }" title="deaf" aria-label="deaf">
|
||||
<i class="fa fa-deaf"/>
|
||||
</span>
|
||||
</t>
|
||||
<t t-if="callParticipantCard.rtcSession.channel.rtc and callParticipantCard.rtcSession.isAudioInError">
|
||||
<span class="o_CallParticipantCard_overlayTopElement d-flex flex-column justify-content-center me-1 p-2 rounded-circle bg-900 text-danger" title="Issue with audio">
|
||||
<i class="fa fa-exclamation-triangle"/>
|
||||
</span>
|
||||
</t>
|
||||
<t t-if="callParticipantCard.rtcSession.channel.rtc and !callParticipantCard.rtcSession.rtcAsCurrentSession and !['connected', 'completed'].includes(callParticipantCard.rtcSession.connectionState)">
|
||||
<span class="o_CallParticipantCard_overlayTopElement d-flex flex-column justify-content-center me-1 p-2 rounded-circle bg-900" t-att-title="callParticipantCard.rtcSession.connectionState">
|
||||
<i class="fa fa-exclamation-triangle o_CallParticipantCard_connectionState text-warning"/>
|
||||
</span>
|
||||
</t>
|
||||
<t t-if="callParticipantCard.rtcSession.isScreenSharingOn and !callParticipantCard.isMinimized and !callParticipantCard.rtcSession.channel.rtc">
|
||||
<span class="o_CallParticipantCard_liveIndicator rounded-pill text-bg-danger d-flex align-items-center me-1 fw-bolder" title="live" aria-label="live">
|
||||
LIVE
|
||||
</span>
|
||||
</t>
|
||||
</div>
|
||||
|
||||
<!-- volume popover -->
|
||||
<t t-if="!callParticipantCard.rtcSession.isOwnSession">
|
||||
<i class="o_CallParticipantCard_volumeMenuAnchor position-absolute bottom-0 start-50" t-on-click="callParticipantCard.onClickVolumeAnchor" t-ref="volumeMenuAnchor"/>
|
||||
</t>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { registerMessagingComponent } from '@mail/utils/messaging_component';
|
||||
|
||||
const { Component } = owl;
|
||||
|
||||
export class CallParticipantCardPopoverContentView extends Component {
|
||||
|
||||
/**
|
||||
* @returns {CallParticipantCardPopoverContentView}
|
||||
*/
|
||||
get callParticipantCardPopoverContentView() {
|
||||
return this.props.record;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Object.assign(CallParticipantCardPopoverContentView, {
|
||||
props: { record: Object },
|
||||
template: 'mail.CallParticipantCardPopoverContentView',
|
||||
});
|
||||
|
||||
registerMessagingComponent(CallParticipantCardPopoverContentView);
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
<t t-name="mail.CallParticipantCardPopoverContentView" owl="1">
|
||||
<div class="d-flex flex-column p-3">
|
||||
<input type="range" min="0.0" max="1" step="0.01" t-att-value="callParticipantCardPopoverContentView.callParticipantCard.rtcSession.volume" t-on-change="callParticipantCardPopoverContentView.onChangeVolume" class="form-range"/>
|
||||
<t t-if="callParticipantCardPopoverContentView.hasConnectionInfo">
|
||||
<hr class="o_CallParticipantCardPopoverContentView_volumeMenuAnchorSeparator w-100 border-top"/>
|
||||
<div t-esc="callParticipantCardPopoverContentView.inboundConnectionTypeText"/>
|
||||
<div t-esc="callParticipantCardPopoverContentView.outboundConnectionTypeText"/>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { useComponentToModel } from '@mail/component_hooks/use_component_to_model';
|
||||
import { useUpdateToModel } from '@mail/component_hooks/use_update_to_model';
|
||||
import { registerMessagingComponent } from '@mail/utils/messaging_component';
|
||||
|
||||
const { Component } = owl;
|
||||
|
||||
export class CallParticipantVideo extends Component {
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
setup() {
|
||||
super.setup();
|
||||
useComponentToModel({ fieldName: 'component' });
|
||||
useUpdateToModel({ methodName: 'onComponentUpdate' });
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Getters / Setters
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @returns {CallParticipantVideoView}
|
||||
*/
|
||||
get callParticipantVideoView() {
|
||||
return this.props.record;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Object.assign(CallParticipantVideo, {
|
||||
props: { record: Object },
|
||||
template: 'mail.CallParticipantVideo',
|
||||
});
|
||||
|
||||
registerMessagingComponent(CallParticipantVideo);
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="mail.CallParticipantVideo" owl="1">
|
||||
<t t-if="callParticipantVideoView">
|
||||
<video class="o_CallParticipantVideo w-100 h-100 rounded-1 cursor-pointer"
|
||||
t-attf-class="{{ className }}"
|
||||
playsinline="true"
|
||||
autoplay="true"
|
||||
muted="true"
|
||||
t-on-loadedmetadata="callParticipantVideoView.onVideoLoadedMetaData"
|
||||
t-ref="root"
|
||||
/>
|
||||
</t>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { registerMessagingComponent } from '@mail/utils/messaging_component';
|
||||
|
||||
const { Component } = owl;
|
||||
|
||||
export class CallSettingsMenu extends Component {
|
||||
|
||||
/**
|
||||
* @returns {CallSettingsMenu}
|
||||
*/
|
||||
get callSettingsMenu() {
|
||||
return this.props.record;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Object.assign(CallSettingsMenu, {
|
||||
props: { record: Object },
|
||||
template: 'mail.CallSettingsMenu',
|
||||
});
|
||||
|
||||
registerMessagingComponent(CallSettingsMenu);
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
// ------------------------------------------------------------------
|
||||
// Layout
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
.o_CallSettingsMenu_optionInputGroupInput {
|
||||
flex-grow: 2;
|
||||
}
|
||||
|
|
@ -0,0 +1,106 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="mail.CallSettingsMenu" owl="1">
|
||||
<div class="o_CallSettingsMenu py-2 user-select-none" t-attf-class="{{ className }}" t-ref="root">
|
||||
<div class="o_CallSettingsMenu_category d-flex flex-column px-3 overflow-auto">
|
||||
<div class="o_CallSettingsMenu_categoryTitle py-2 fw-bolder text-700 text-truncate text-uppercase">Voice Settings</div>
|
||||
<div class="o_CallSettingsMenu_option mb-3 d-flex align-items-center flex-wrap">
|
||||
<label class="o_CallSettingsMenu_optionLabel d-flex align-items-center flex-wrap mw-100 cursor-pointer" title="Input device" aria-label="Input device">
|
||||
<span class="o_CallSettingsMenuoptionName me-2 text-truncate">Input device</span>
|
||||
<div>
|
||||
<select name="inputDevice" class="o_CallSettingsMenu_optionDeviceSelect form-select" t-att-value="messaging.userSetting.audioInputDeviceId" t-on-change="callSettingsMenu.onChangeSelectAudioInput">
|
||||
<option value="">Browser default</option>
|
||||
<t t-foreach="callSettingsMenu.userDevices" t-as="device" t-key="device_index">
|
||||
<CallSettingsMenuDevice device="device"/>
|
||||
</t>
|
||||
</select>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
<div class="o_CallSettingsMenu_option mb-3 d-flex align-items-center flex-wrap">
|
||||
<label class="o_CallSettingsMenu_optionLabel o_CallSettingsMenu_pushToTalkOption d-flex align-items-center flex-wrap mw-100 cursor-pointer" title="Enable Push-to-talk" aria-label="Enable Push-to-talk">
|
||||
<input type="checkbox" aria-label="toggle push-to-talk" title="toggle push-to-talk" t-on-change="callSettingsMenu.onChangePushToTalk" t-att-checked="messaging.userSetting.usePushToTalk ? 'checked' : ''" class="form-check-input"/>
|
||||
<span class="o_CallSettingsMenu_optionName ms-2 text-truncate">Enable Push-to-talk</span>
|
||||
</label>
|
||||
</div>
|
||||
<t t-if="messaging.userSetting.usePushToTalk">
|
||||
<div class="o_CallSettingsMenu_option mb-3 d-flex align-items-center flex-wrap">
|
||||
<label class="o_CallSettingsMenu_optionLabel o_CallSettingsMenu_pushToTalkKeyOption d-flex align-items-center flex-wrap mw-100 cursor-pointer" title="Push-to-talk key" aria-label="Push-to-talk key">
|
||||
<span class="o_CallSettingsMenu_optionName me-2 text-truncate">Push-to-talk key</span>
|
||||
<span class="o_CallSettingsMenu_optionPushToTalkGroup d-flex">
|
||||
<t t-if="messaging.userSetting.pushToTalkKey">
|
||||
<span class="o_CallSettingsMenu_optionPushToTalkGroupKey ms-1 px-3 border border-2 rounded fs-3" t-attf-class="{{ callSettingsMenu.userSetting.isRegisteringKey ? 'o-isRegistering border-danger' : 'border-primary' }}" t-esc="messaging.userSetting.pushToTalkKeyToString()"/>
|
||||
</t>
|
||||
<button class="o_CallSettingsMenu_button btn btn-link px-2 py-0 text-black" t-on-click="callSettingsMenu.onClickRegisterKeyButton">
|
||||
<t t-if="callSettingsMenu.userSetting.isRegisteringKey">
|
||||
<i title="Cancel" aria-label="Cancel" class="fa fa-2x fa-times-circle"/>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<i title="Register new key" aria-label="Register new key" class="fa fa-2x fa-keyboard-o"/>
|
||||
</t>
|
||||
</button>
|
||||
</span>
|
||||
</label>
|
||||
|
||||
</div>
|
||||
<div t-if="callSettingsMenu.userSetting.isRegisteringKey">Press a key to register it as the push-to-talk shortcut</div>
|
||||
<div class="o_CallSettingsMenu_option mb-3 d-flex align-items-center flex-wrap">
|
||||
<label class="o_CallSettingsMenu_optionLabel o_CallSettingsMenu_pushToTalkDelayOption d-flex align-items-center flex-wrap mw-100 cursor-pointer" title="Delay after releasing push-to-talk" aria-label="Delay after releasing push-to-talk">
|
||||
<span class="o_CallSettingsMenu_optionName me-2 text-truncate">Delay after releasing push-to-talk</span>
|
||||
<div class="o_CallSettingsMenu_optionInputGroup d-flex w-100">
|
||||
<input class="o_CallSettingsMenu_optionInputGroupInput form-range" type="range" min="1" max="2000" step="1" t-att-value="messaging.userSetting.voiceActiveDuration" t-on-change="callSettingsMenu.onChangeDelay"/>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<div class="o_CallSettingsMenu_option mb-3 d-flex align-items-center flex-wrap">
|
||||
<label class="o_CallSettingsMenu_optionLabel o_CallSettingsMenu_voiceThresholdOption d-flex align-items-center flex-wrap mw-100 cursor-pointer" title="Voice detection threshold" aria-label="Voice detection threshold">
|
||||
<span class="o_CallSettingsMenu_optionName me-2 text-truncate">Voice detection threshold</span>
|
||||
<div class="o_CallSettingsMenu_optionInputGroup d-flex w-100">
|
||||
<input class="o_CallSettingsMenu_optionInputGroupInput form-range" type="range" min="0.001" max="1" step="0.001" t-att-value="messaging.userSetting.voiceActivationThreshold" t-on-change="callSettingsMenu.onChangeThreshold"/>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
<div class="o_CallSettingsMenu_category d-flex flex-column px-3 overflow-auto">
|
||||
<div class="o_CallSettingsMenu_categoryTitle py-2 fw-bolder text-700 text-truncate text-uppercase">Video Settings</div>
|
||||
<div class="o_CallSettingsMenu_option mb-3 d-flex align-items-center flex-wrap">
|
||||
<label class="o_CallSettingsMenu_optionLabel o_CallSettingsMenu_showOnlyVideoOption d-flex align-items-center flex-wrap mw-100 cursor-pointer" title="Show video participants only" aria-label="Show video participants only">
|
||||
<input type="checkbox" aria-label="toggle push-to-talk" title="Show video participants only" t-on-change="callSettingsMenu.onChangeVideoFilterCheckbox" t-att-checked="callSettingsMenu.thread.channel.showOnlyVideo ? 'checked' : ''" class="form-check-input"/>
|
||||
<span class="o_CallSettingsMenu_optionName ms-2 text-truncate">Show video participants only</span>
|
||||
</label>
|
||||
</div>
|
||||
<t t-if="messaging.device.hasCanvasFilterSupport">
|
||||
<div class="o_CallSettingsMenu_option mb-3 d-flex align-items-center flex-wrap">
|
||||
<label class="o_CallSettingsMenu_optionLabel o_CallSettingsMenu_blurOption d-flex align-items-center flex-wrap mw-100 cursor-pointer" title="Blur video background" aria-label="Blur video background">
|
||||
<input type="checkbox" aria-label="Blur video background" title="Blur video background" t-on-change="callSettingsMenu.onChangeBlur" t-att-checked="messaging.userSetting.useBlur ? 'checked' : ''" class="form-check-input"/>
|
||||
<span class="o_CallSettingsMenu_optionName ms-2 text-truncate">Blur video background</span>
|
||||
</label>
|
||||
</div>
|
||||
</t>
|
||||
<t t-if="messaging.userSetting.useBlur">
|
||||
<div class="o_CallSettingsMenu_option mb-3 d-flex align-items-center flex-wrap">
|
||||
<label class="o_CallSettingsMenu_optionLabel o_CallSettingsMenu_backgroundBlurIntensityOption d-flex align-items-center flex-wrap mw-100 cursor-pointer" title="Background blur intensity" aria-label="Background blur intensity">
|
||||
<span class="o_CallSettingsMenu_optionName me-2 text-truncate">Background blur intensity</span>
|
||||
<div class="o_CallSettingsMenu_optionInputGroup d-flex w-100">
|
||||
<input class="o_CallSettingsMenu_optionInputGroupInput form-range" type="range" min="0" max="20" step="1" t-att-value="messaging.userSetting.backgroundBlurAmount" t-on-change="callSettingsMenu.onChangeBackgroundBlurAmount"/>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
<div class="o_CallSettingsMenu_option mb-3 d-flex align-items-center flex-wrap">
|
||||
<label class="o_CallSettingsMenu_optionLabel o_CallSettingsMenu_edgeBlurIntensityOption d-flex align-items-center flex-wrap mw-100 cursor-pointer" title="Edge blur intensity" aria-label="Edge blur intensity">
|
||||
<span class="o_CallSettingsMenu_optionName me-2 text-truncate">Edge blur intensity</span>
|
||||
<div class="o_CallSettingsMenu_optionInputGroup d-flex w-100">
|
||||
<input class="o_CallSettingsMenu_optionInputGroupInput form-range" type="range" min="0" max="20" step="1" t-att-value="messaging.userSetting.edgeBlurAmount" t-on-change="callSettingsMenu.onChangeEdgeBlurAmount"/>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { registerMessagingComponent } from '@mail/utils/messaging_component';
|
||||
|
||||
const { Component } = owl;
|
||||
|
||||
export class CallSettingsMenuDevice extends Component {}
|
||||
|
||||
Object.assign(CallSettingsMenuDevice, {
|
||||
props: { device: Object },
|
||||
template: 'mail.CallSettingsMenuDevice',
|
||||
});
|
||||
|
||||
registerMessagingComponent(CallSettingsMenuDevice);
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="mail.CallSettingsMenuDevice" owl="1">
|
||||
<t t-if="props.device.kind === 'audioinput'">
|
||||
<option t-att-value="props.device.deviceId" t-attf-class="{{ className }}" t-ref="root"><t t-esc="props.device.label"/></option>
|
||||
</t>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { registerMessagingComponent } from '@mail/utils/messaging_component';
|
||||
|
||||
const { Component } = owl;
|
||||
|
||||
export class CallSidebarView extends Component {
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Getters / Setters
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @returns {CallSidebarView}
|
||||
*/
|
||||
get callSidebarView() {
|
||||
return this.props.record;
|
||||
}
|
||||
}
|
||||
|
||||
Object.assign(CallSidebarView, {
|
||||
props: { record: Object },
|
||||
template: 'mail.CallSidebarView',
|
||||
});
|
||||
|
||||
registerMessagingComponent(CallSidebarView);
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
// ------------------------------------------------------------------
|
||||
// Layout
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
.o_CallSidebarView {
|
||||
width: 120px;
|
||||
min-width: 120px;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 0.3vw;
|
||||
}
|
||||
}
|
||||
|
||||
.o_CallSidebarView_participantCard {
|
||||
aspect-ratio: 16/9;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Style
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
.o_CallSidebarView {
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
background: map-get($grays, '900');
|
||||
}
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: map-get($grays, '700');
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="mail.CallSidebarView" owl="1">
|
||||
<t t-if="callSidebarView">
|
||||
<div class="o_CallSidebarView d-flex align-items-center h-100 flex-column">
|
||||
<t t-foreach="callSidebarView.sidebarTiles" t-as="tile" t-key="tile.localId">
|
||||
<CallParticipantCard
|
||||
className="'o_CallSidebarView_participantCard w-100'"
|
||||
record="tile.participantCard"
|
||||
/>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { registerMessagingComponent } from '@mail/utils/messaging_component';
|
||||
|
||||
const { Component } = owl;
|
||||
|
||||
export class CallSystrayMenu extends Component {
|
||||
|
||||
/**
|
||||
* @returns {CallSystrayMenu}
|
||||
*/
|
||||
get callSystrayMenu() {
|
||||
return this.props.record;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Object.assign(CallSystrayMenu, {
|
||||
props: { record: Object },
|
||||
template: 'mail.CallSystrayMenu',
|
||||
});
|
||||
|
||||
registerMessagingComponent(CallSystrayMenu);
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
// ------------------------------------------------------------------
|
||||
// Layout
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
.o_CallSystrayMenu_buttonContent {
|
||||
max-width: 150px;
|
||||
}
|
||||
|
||||
.o_CallSystrayMenu_dot {
|
||||
animation: flash 3s ease infinite;
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="mail.CallSystrayMenu" owl="1">
|
||||
<div class="o_CallSystrayMenu dropdown" t-attf-class="{{ className }}" t-ref="root">
|
||||
<t t-if="messaging">
|
||||
<CallInviteRequestPopupList/>
|
||||
<t t-if="messaging.rtc.channel">
|
||||
<button class="o_CallSystrayMenu_button px-3 user-select-none dropdown-toggle o-no-caret o-dropdown--narrow" t-att-title="callSystrayMenu.buttonTitle" role="button" t-on-click="messaging.rtc.onClickActivityNoticeButton">
|
||||
<div class="o_CallSystrayMenu_buttonContent d-flex align-items-center">
|
||||
<span class="position-relative me-2">
|
||||
<i class="o_CallSystrayMenu_outputIndicator fa me-2" t-att-class="{
|
||||
'fa-microphone': !messaging.rtc.sendDisplay and !messaging.rtc.sendUserVideo,
|
||||
'fa-video-camera': messaging.rtc.sendUserVideo,
|
||||
'fa-desktop': messaging.rtc.sendDisplay,
|
||||
}"/>
|
||||
<small class="position-absolute top-0 end-0 bottom-0 mt-n3 pt-1">
|
||||
<i class="o_CallSystrayMenu_dot fa fa-circle text-warning small"/>
|
||||
</small>
|
||||
</span>
|
||||
<em class="o_CallSystrayMenu_buttonTitle text-truncate" t-esc="messaging.rtc.channel.displayName"/>
|
||||
</div>
|
||||
</button>
|
||||
</t>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { useModels } from '@mail/component_hooks/use_models';
|
||||
// ensure components are registered beforehand.
|
||||
import '@mail/components/call_systray_menu/call_systray_menu';
|
||||
import { getMessagingComponent } from "@mail/utils/messaging_component";
|
||||
|
||||
const { Component } = owl;
|
||||
|
||||
export class CallSystrayMenuContainer extends Component {
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
setup() {
|
||||
useModels();
|
||||
super.setup();
|
||||
}
|
||||
|
||||
get messaging() {
|
||||
return this.env.services.messaging.modelManager.messaging;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Object.assign(CallSystrayMenuContainer, {
|
||||
components: { CallSystrayMenu: getMessagingComponent('CallSystrayMenu') },
|
||||
template: 'mail.CallSystrayMenuContainer',
|
||||
});
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="mail.CallSystrayMenuContainer" owl="1">
|
||||
<div class="CallSystrayMenuContainer">
|
||||
<t t-if="messaging">
|
||||
<CallSystrayMenu record="messaging.rtc.callSystrayMenu"/>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
// = Call View
|
||||
// ============================================================================
|
||||
// No CSS hacks, variables overrides only
|
||||
|
||||
.o_CallView {
|
||||
--CallView-background-color: #{$o-gray-100};
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { registerMessagingComponent } from '@mail/utils/messaging_component';
|
||||
|
||||
const { Component } = owl;
|
||||
|
||||
// TODO a nice-to-have would be a resize handle under the videos.
|
||||
|
||||
export class CallView extends Component {
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Getters / Setters
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @returns {CallView}
|
||||
*/
|
||||
get callView() {
|
||||
return this.props.record;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Object.assign(CallView, {
|
||||
props: { record: Object },
|
||||
template: 'mail.CallView',
|
||||
});
|
||||
|
||||
registerMessagingComponent(CallView);
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
// ------------------------------------------------------------------
|
||||
// Layout
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
.o_CallView {
|
||||
height: 50%; // ensures that the view returns to the right height when resized
|
||||
min-height: 50%;
|
||||
background: var(--CallView-background-color, #{$dark});
|
||||
|
||||
&.o-isMinimized {
|
||||
height: 20%; // ensures that the view returns to the right height when resized
|
||||
min-height: #{"max(20%, 130px)"};
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="mail.CallView" owl="1">
|
||||
<t t-if="callView">
|
||||
<div class="o_CallView d-flex" t-att-class="{'o-fullScreen fixed-top vw-100 vh-100': callView.isFullScreen, 'o-isMinimized': callView.isMinimized, 'position-relative': !callView.isFullScreen }" t-attf-class="{{ className }}" t-ref="root">
|
||||
<!-- Used to make the component depend on the window size and trigger an update when the window size changes. -->
|
||||
|
||||
<CallMainView record="callView.callMainView"/>
|
||||
<CallSidebarView t-if="callView.callSidebarView" record="callView.callSidebarView"/>
|
||||
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { useComponentToModel } from '@mail/component_hooks/use_component_to_model';
|
||||
import { useRefToModel } from '@mail/component_hooks/use_ref_to_model';
|
||||
import { useUpdateToModel } from '@mail/component_hooks/use_update_to_model';
|
||||
import { registerMessagingComponent } from '@mail/utils/messaging_component';
|
||||
|
||||
const { Component } = owl;
|
||||
|
||||
export class ChannelInvitationForm extends Component {
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
setup() {
|
||||
super.setup();
|
||||
useComponentToModel({ fieldName: 'component' });
|
||||
useRefToModel({ fieldName: 'searchInputRef', refName: 'searchInput' });
|
||||
useUpdateToModel({ methodName: 'onComponentUpdate' });
|
||||
}
|
||||
|
||||
get channelInvitationForm() {
|
||||
return this.props.record;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Object.assign(ChannelInvitationForm, {
|
||||
props: { record: Object },
|
||||
template: 'mail.ChannelInvitationForm',
|
||||
});
|
||||
|
||||
registerMessagingComponent(ChannelInvitationForm);
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
// ------------------------------------------------------------------
|
||||
// Layout
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
.o_ChannelInvitationForm_selectedPartners {
|
||||
max-height: 100px;
|
||||
}
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="mail.ChannelInvitationForm" owl="1">
|
||||
<t t-if="channelInvitationForm">
|
||||
<div class="o_ChannelInvitationForm d-flex flex-column" t-attf-class="{{ className }}" t-ref="root">
|
||||
<h3 class="mx-3 mt-3 mb-2">Invite people</h3>
|
||||
<t t-if="!messaging.isCurrentUserGuest">
|
||||
<div class="mx-3 my-2">
|
||||
<input class="o_ChannelInvitationForm_searchInput form-control" type="text" t-att-value="channelInvitationForm.searchTerm" placeholder="Type the name of a person" t-on-input="channelInvitationForm.onInputSearch" t-ref="searchInput"/>
|
||||
</div>
|
||||
<div class="d-flex flex-column flex-grow-1 mx-0 py-2 overflow-auto">
|
||||
<t t-foreach="channelInvitationForm.selectablePartnerViews" t-as="selectablePartnerView" t-key="selectablePartnerView.localId">
|
||||
<ChannelInvitationFormSelectablePartner record="selectablePartnerView"/>
|
||||
</t>
|
||||
<t t-if="channelInvitationForm.selectablePartners.length === 0">
|
||||
<div class="mx-3">No user found that is not already a member of this channel.</div>
|
||||
</t>
|
||||
<t t-if="channelInvitationForm.searchResultCount > channelInvitationForm.selectablePartners.length">
|
||||
<div class="mx-3">
|
||||
Showing <t t-esc="channelInvitationForm.selectablePartners.length"/> results out of <t t-esc="channelInvitationForm.searchResultCount"/>. Narrow your search to see more choices.
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
<t t-if="channelInvitationForm.selectedPartners.length > 0">
|
||||
<div class="mx-3 mt-3">
|
||||
<h4>Selected users:</h4>
|
||||
<div class="o_ChannelInvitationForm_selectedPartners overflow-auto">
|
||||
<t t-foreach="channelInvitationForm.selectedPartnerViews" t-as="selectedPartnerView" t-key="selectedPartnerView.localId">
|
||||
<ChannelInvitationFormSelectedPartner record="selectedPartnerView"/>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
<div class="mx-3 mt-2 mb-3">
|
||||
<button class="o_ChannelInvitationForm_inviteButton btn btn-primary w-100" t-att-disabled="channelInvitationForm.selectedPartners.length === 0" t-on-click="channelInvitationForm.onClickInvite">
|
||||
<t t-esc="channelInvitationForm.inviteButtonText"/>
|
||||
</button>
|
||||
</div>
|
||||
</t>
|
||||
<t t-if="channelInvitationForm.thread and channelInvitationForm.thread.invitationLink">
|
||||
<h4 class="mx-3 mt-3 mb-2">Invitation Link</h4>
|
||||
<div class="mx-3 mt-2 mb-3">
|
||||
<div class="input-group">
|
||||
<input class="form-control" type="text" t-att-value="channelInvitationForm.thread.invitationLink" readonly="" t-on-focus="channelInvitationForm.onFocusInvitationLinkInput" />
|
||||
<button class="btn btn-primary" t-on-click="channelInvitationForm.onClickCopy">
|
||||
<i class="fa fa-copy"/>
|
||||
</button>
|
||||
</div>
|
||||
<t t-if="channelInvitationForm.accessRestrictedToGroupText">
|
||||
<div class="o_ChannelInvitationForm_accessRestrictedToGroup mt-2" t-esc="channelInvitationForm.accessRestrictedToGroupText"/>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { registerMessagingComponent } from '@mail/utils/messaging_component';
|
||||
|
||||
const { Component } = owl;
|
||||
|
||||
export class ChannelInvitationFormSelectablePartner extends Component {
|
||||
|
||||
/**
|
||||
* @returns {ChannelInvitationFormSelectablePartnerView}
|
||||
*/
|
||||
get channelInvitationFormSelectablePartnerView() {
|
||||
return this.props.record;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Object.assign(ChannelInvitationFormSelectablePartner, {
|
||||
props: { record: Object },
|
||||
template: 'mail.ChannelInvitationFormSelectablePartner',
|
||||
});
|
||||
|
||||
registerMessagingComponent(ChannelInvitationFormSelectablePartner);
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
|
||||
.o_ChannelInvitationFormSelectablePartner_avatar {
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.o_ChannelInvitationFormSelectablePartner_avatarContainer {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.o_ChannelInvitationForm_selectablePartnerName {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="mail.ChannelInvitationFormSelectablePartner" owl="1">
|
||||
<div class="o_ChannelInvitationFormSelectablePartner d-flex align-items-center px-3 py-1 btn-light" t-on-click="() => channelInvitationFormSelectablePartnerView.channelInvitationFormOwner.onClickSelectablePartner(channelInvitationFormSelectablePartnerView.partner)" t-att-data-partner-id="channelInvitationFormSelectablePartnerView.partner.id" t-attf-class="{{ className }}" t-ref="root">
|
||||
<div class="o_ChannelInvitationFormSelectablePartner_avatarContainer position-relative flex-shrink-0">
|
||||
<img class="o_ChannelInvitationFormSelectablePartner_avatar w-100 h-100 rounded-circle" t-att-src="channelInvitationFormSelectablePartnerView.partner.avatarUrl" alt="Avatar"/>
|
||||
<t t-if="channelInvitationFormSelectablePartnerView.personaImStatusIconView">
|
||||
<PersonaImStatusIcon
|
||||
className="'o_ChannelInvitationFormSelectablePartner_imStatusIcon position-absolute bottom-0 end-0 d-flex align-items-center justify-content-center text-white'"
|
||||
classNameObj="{
|
||||
'o_ChannelInvitationFormSelectablePartner_imStatusIcon-mobile': messaging.device.isSmall,
|
||||
'small' : !messaging.device.isSmall,
|
||||
}"
|
||||
record="channelInvitationFormSelectablePartnerView.personaImStatusIconView"
|
||||
/>
|
||||
</t>
|
||||
</div>
|
||||
<span class="o_ChannelInvitationFormSelectablePartner_name flex-grow-1 mx-2 text-truncate">
|
||||
<t t-esc="channelInvitationFormSelectablePartnerView.partner.nameOrDisplayName"/>
|
||||
</span>
|
||||
<input class="o_ChannelInvitationFormSelectablePartner_checkbox form-check-input flex-shrink-0" type="checkbox" t-att-checked="channelInvitationFormSelectablePartnerView.channelInvitationFormOwner.selectedPartners.includes(channelInvitationFormSelectablePartnerView.partner) ? 'checked' : undefined" t-on-input="ev => channelInvitationFormSelectablePartnerView.channelInvitationFormOwner.onInputPartnerCheckbox(channelInvitationFormSelectablePartnerView.partner, ev)" t-ref="selection-status"/>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { registerMessagingComponent } from '@mail/utils/messaging_component';
|
||||
|
||||
const { Component } = owl;
|
||||
|
||||
export class ChannelInvitationFormSelectedPartner extends Component {
|
||||
|
||||
/**
|
||||
* @returns {ChannelInvitationFormSelectedPartnerView}
|
||||
*/
|
||||
get channelInvitationFormSelectedPartnerView() {
|
||||
return this.props.record;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Object.assign(ChannelInvitationFormSelectedPartner, {
|
||||
props: { record: Object },
|
||||
template: 'mail.ChannelInvitationFormSelectedPartner',
|
||||
});
|
||||
|
||||
registerMessagingComponent(ChannelInvitationFormSelectedPartner);
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="mail.ChannelInvitationFormSelectedPartner" owl="1">
|
||||
<button class="btn btn-secondary" t-on-click="() => channelInvitationFormSelectedPartnerView.channelInvitationFormOwner.onClickSelectedPartner(channelInvitationFormSelectedPartnerView.partner)">
|
||||
<t t-esc="channelInvitationFormSelectedPartnerView.partner.nameOrDisplayName"/> <i class="fa fa-times"/>
|
||||
</button>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { registerMessagingComponent } from '@mail/utils/messaging_component';
|
||||
|
||||
const { Component } = owl;
|
||||
|
||||
export class ChannelMember extends Component {
|
||||
|
||||
/**
|
||||
* @returns {ChannelMemberView}
|
||||
*/
|
||||
get channelMemberView() {
|
||||
return this.props.record;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Object.assign(ChannelMember, {
|
||||
props: {
|
||||
record: Object,
|
||||
},
|
||||
template: 'mail.ChannelMember',
|
||||
});
|
||||
|
||||
registerMessagingComponent(ChannelMember);
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
.o_ChannelMember:hover {
|
||||
background-color: map-get($grays, '300');
|
||||
}
|
||||
|
||||
.o_ChannelMember_avatar {
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.o_ChannelMember_avatarContainer {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.o_ChannelMember_name {
|
||||
min-width: 0;
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="mail.ChannelMember" owl="1">
|
||||
<div class="o_ChannelMember d-flex align-items-center mx-2 p-2" t-att-class="{'cursor-pointer': channelMemberView.hasOpenChat}" t-attf-class="{{ className }}" t-att-data-partner-id="channelMemberView.channelMember.persona.partner and channelMemberView.channelMember.persona.partner.id" t-att-title="channelMemberView.memberTitleText" t-on-click="channelMemberView.onClickMember" t-ref="root">
|
||||
<div class="o_ChannelMember_avatarContainer position-relative flex-shrink-0">
|
||||
<img class="o_ChannelMember_avatar rounded-circle w-100 h-100" t-att-src="channelMemberView.channelMember.avatarUrl" alt="Avatar"/>
|
||||
|
||||
<t t-if="channelMemberView.personaImStatusIconView">
|
||||
<PersonaImStatusIcon
|
||||
className="'o_ChannelMember_personaImStatusIcon position-absolute bottom-0 end-0 d-flex align-items-center justify-content-center text-light'"
|
||||
classNameObj="{
|
||||
'o-isDeviceSmall': messaging.device.isSmall,
|
||||
'small': !messaging.device.isSmall,
|
||||
}"
|
||||
hasOpenChat="channelMemberView.hasOpenChat"
|
||||
record="channelMemberView.personaImStatusIconView"
|
||||
/>
|
||||
</t>
|
||||
</div>
|
||||
<span class="o_ChannelMember_name ms-2 flex-column-1 text-truncate">
|
||||
<t t-esc="channelMemberView.channelMember.channel.thread.getMemberName(channelMemberView.channelMember.persona)"/>
|
||||
</span>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { registerMessagingComponent } from '@mail/utils/messaging_component';
|
||||
|
||||
const { Component } = owl;
|
||||
|
||||
export class ChannelMemberList extends Component {
|
||||
|
||||
/**
|
||||
* @returns {ChannelMemberListView}
|
||||
*/
|
||||
get channelMemberListView() {
|
||||
return this.props.record;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Object.assign(ChannelMemberList, {
|
||||
props: { record: Object },
|
||||
template: 'mail.ChannelMemberList',
|
||||
});
|
||||
|
||||
registerMessagingComponent(ChannelMemberList);
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="mail.ChannelMemberList" owl="1">
|
||||
<t t-if="channelMemberListView">
|
||||
<div class="o_ChannelMemberList d-flex flex-column overflow-auto bg-light" t-attf-class="{{ className }}" t-ref="root">
|
||||
<t t-if="channelMemberListView.onlineCategoryView">
|
||||
<ChannelMemberListCategory record="channelMemberListView.onlineCategoryView"/>
|
||||
</t>
|
||||
<t t-if="channelMemberListView.offlineCategoryView">
|
||||
<ChannelMemberListCategory record="channelMemberListView.offlineCategoryView"/>
|
||||
</t>
|
||||
<t t-if="channelMemberListView.channel.unknownMemberCount === 1">
|
||||
<span class="mx-2 mt-2">And 1 other member.</span>
|
||||
</t>
|
||||
<t t-if="channelMemberListView.channel.unknownMemberCount > 1">
|
||||
<span class="mx-2 mt-2">And <t t-esc="channelMemberListView.channel.unknownMemberCount"/> other members.</span>
|
||||
</t>
|
||||
<t t-if="!channelMemberListView.channel.areAllMembersLoaded">
|
||||
<div class="mx-2 my-1">
|
||||
<button class="o_ChannelMemberList_loadMoreButton btn btn-secondary" t-on-click="channelMemberListView.onClickLoadMoreMembers">Load more</button>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { registerMessagingComponent } from '@mail/utils/messaging_component';
|
||||
|
||||
const { Component } = owl;
|
||||
|
||||
export class ChannelMemberListCategory extends Component {
|
||||
|
||||
/**
|
||||
* @returns {ChannelMemberListCategoryView}
|
||||
*/
|
||||
get record() {
|
||||
return this.props.record;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Object.assign(ChannelMemberListCategory, {
|
||||
props: { record: Object },
|
||||
template: 'mail.ChannelMemberListCategory',
|
||||
});
|
||||
|
||||
registerMessagingComponent(ChannelMemberListCategory);
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="mail.ChannelMemberListCategory" owl="1">
|
||||
<h6 class="m-2" t-esc="record.title"/>
|
||||
<t t-foreach="record.channelMemberViews" t-as="channelMemberView" t-key="channelMemberView.localId">
|
||||
<ChannelMember record="channelMemberView"/>
|
||||
</t>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { useRefToModel } from '@mail/component_hooks/use_ref_to_model';
|
||||
import { registerMessagingComponent } from '@mail/utils/messaging_component';
|
||||
|
||||
const { Component } = owl;
|
||||
|
||||
export class ChannelPreviewView extends Component {
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
setup() {
|
||||
super.setup();
|
||||
useRefToModel({ fieldName: 'markAsReadRef', refName: 'markAsRead' });
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {ChannelPreviewView}
|
||||
*/
|
||||
get channelPreviewView() {
|
||||
return this.props.record;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Object.assign(ChannelPreviewView, {
|
||||
props: { record: Object },
|
||||
template: 'mail.ChannelPreviewView',
|
||||
});
|
||||
|
||||
registerMessagingComponent(ChannelPreviewView);
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="mail.ChannelPreviewView" owl="1">
|
||||
<t t-if="channelPreviewView">
|
||||
<!--
|
||||
The preview template is used by the discuss in mobile, and by the systray
|
||||
menu in order to show preview of threads.
|
||||
-->
|
||||
<div class="o_NotificationListItem o_ChannelPreviewView d-flex flex-shrink-0 align-items-center p-1 cursor-pointer"
|
||||
t-att-class="{ 'o-muted': channelPreviewView.channel.localMessageUnreadCounter === 0 }"
|
||||
t-attf-class="{{ className }}"
|
||||
t-on-click="channelPreviewView.onClick"
|
||||
t-att-data-channel-id="channelPreviewView.channel.id"
|
||||
t-ref="root"
|
||||
>
|
||||
<div class="o_NotificationListItem_sidebar o_ChannelPreviewView_sidebar m-1">
|
||||
<div class="o_NotificationListItem_imageContainer o_ChannelPreviewView_imageContainer o_ChannelPreviewView_sidebarItem position-relative">
|
||||
<img class="o_NotificationListItem_image o_ChannelPreviewView_image w-100 h-100 rounded-circle" t-att-src="channelPreviewView.imageUrl" alt="Thread Image"/>
|
||||
<t t-if="channelPreviewView.personaImStatusIconView">
|
||||
<PersonaImStatusIcon
|
||||
className="'o_NotificationListItem_personaImStatusIcon o_ChannelPreviewView_personaImStatusIcon position-absolute bottom-0 end-0 d-flex align-items-center justify-content-center'"
|
||||
classNameObj="{
|
||||
'o-isDeviceSmall': messaging.device.isSmall,
|
||||
'small': !messaging.device.isSmall,
|
||||
'o-muted': channelPreviewView.channel.localMessageUnreadCounter === 0,
|
||||
}"
|
||||
record="channelPreviewView.personaImStatusIconView"
|
||||
/>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
<div class="o_NotificationListItem_content o_ChannelPreviewView_content d-flex flex-column flex-grow-1 align-self-start m-2">
|
||||
<div class="o_NotificationListItem_header o_ChannelPreviewView_header d-flex align-items-baseline">
|
||||
<span class="o_NotificationListItem_name o_ChannelPreviewView_name text-truncate fw-bold" t-att-class="{ 'o-isDeviceSmall fs-5': messaging.device.isSmall, 'o-muted text-600': channelPreviewView.channel.localMessageUnreadCounter === 0 }">
|
||||
<t t-esc="channelPreviewView.channel.displayName"/>
|
||||
</span>
|
||||
<t t-if="channelPreviewView.channel.localMessageUnreadCounter > 1">
|
||||
<span class="o_NotificationListItem_counter o_ChannelPreviewView_counter mx-1 fw-bold">
|
||||
(<t t-esc="channelPreviewView.channel.localMessageUnreadCounter"/>)
|
||||
</span>
|
||||
</t>
|
||||
<t t-if="channelPreviewView.thread.rtcSessions.length > 0">
|
||||
<span class="o_ChannelPreviewView_callIndicator fa fa-volume-up mx-2" t-att-class="{ 'o-isCalling text-danger': channelPreviewView.thread.rtc }"/>
|
||||
</t>
|
||||
<span class="flex-grow-1"/>
|
||||
<t t-if="channelPreviewView.thread.lastMessage and channelPreviewView.thread.lastMessage.date">
|
||||
<small class="o_NotificationListItem_date o_ChannelPreviewView_date flex-shrink-0 text-500" t-att-class="{ 'o-muted': channelPreviewView.channel.localMessageUnreadCounter === 0 }">
|
||||
<t t-esc="channelPreviewView.thread.lastMessage.date.fromNow()"/>
|
||||
</small>
|
||||
</t>
|
||||
</div>
|
||||
<div class="o_ChannelPreviewView_core d-flex align-items-baseline">
|
||||
<span class="o_NotificationListItem_coreItem o_NotificationListItem_inlineText o_ChannelPreviewView_coreItem o_ChannelPreviewView_inlineText me-2 text-truncate" t-att-class="{ 'o-empty': channelPreviewView.isEmpty }">
|
||||
<t t-if="channelPreviewView.lastTrackingValue">
|
||||
<TrackingValue value="channelPreviewView.lastTrackingValue"/>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<t t-if="channelPreviewView.messageAuthorPrefixView">
|
||||
<MessageAuthorPrefix record="channelPreviewView.messageAuthorPrefixView"/>
|
||||
</t>
|
||||
<span class="o_ChannelPreviewView_messageBody" t-esc="channelPreviewView.inlineLastMessageBody"/>
|
||||
</t>
|
||||
</span>
|
||||
<span class="flex-grow-1"/>
|
||||
<t t-if="channelPreviewView.channel.localMessageUnreadCounter > 0">
|
||||
<span class="o_NotificationListItem_coreItem o_NotificationListItem_markAsRead o_ChannelPreviewView_coreItem o_ChannelPreviewView_markAsRead fa fa-check d-flex flex-shrink-0 ms-2 text-600 opacity-50 opacity-100-hover" title="Mark as Read" t-on-click="channelPreviewView.onClickMarkAsRead" t-ref="markAsRead"/>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { useUpdate } from '@mail/component_hooks/use_update';
|
||||
import { registerMessagingComponent } from '@mail/utils/messaging_component';
|
||||
|
||||
const { Component } = owl;
|
||||
|
||||
export class ChatWindow extends Component {
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
setup() {
|
||||
super.setup();
|
||||
useUpdate({ func: () => this._update() });
|
||||
/**
|
||||
* Reference of the autocomplete input (new_message chat window only).
|
||||
* Useful when focusing this chat window, which consists of focusing
|
||||
* this input.
|
||||
*/
|
||||
this._inputRef = { el: null };
|
||||
// the following are passed as props to children
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Public
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @returns {ChatWindow}
|
||||
*/
|
||||
get chatWindow() {
|
||||
return this.props.record;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_update() {
|
||||
if (!this.root.el) {
|
||||
return;
|
||||
}
|
||||
if (this.chatWindow.isDoFocus) {
|
||||
this.chatWindow.update({ isDoFocus: false });
|
||||
if (
|
||||
this.chatWindow.newMessageAutocompleteInputView &&
|
||||
this.chatWindow.newMessageAutocompleteInputView.component &&
|
||||
this.chatWindow.newMessageAutocompleteInputView.component.root.el
|
||||
) {
|
||||
this.chatWindow.newMessageAutocompleteInputView.component.root.el.focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Object.assign(ChatWindow, {
|
||||
props: { record: Object },
|
||||
template: 'mail.ChatWindow',
|
||||
});
|
||||
|
||||
registerMessagingComponent(ChatWindow);
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
// ------------------------------------------------------------------
|
||||
// Layout
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
.o_ChatWindow {
|
||||
overflow: auto;
|
||||
z-index: $zindex-dropdown;
|
||||
|
||||
&:not(.o-isDeviceSmall) {
|
||||
width: $o-mail-thread-window-width;
|
||||
|
||||
&.o-folded {
|
||||
height: $o-mail-chat-window-header-height;
|
||||
}
|
||||
|
||||
&:not(.o-folded) {
|
||||
height: 460px;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.o_ChatWindow_channelInvitationForm {
|
||||
min-height: 450px; // allow flex shrink smaller than content (but not too small)
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Style
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
.o_ChatWindow {
|
||||
background-color: $o-mail-thread-window-bg;
|
||||
box-shadow: -5px -5px 10px rgba(#000000, 0.09);
|
||||
outline: none;
|
||||
|
||||
&.o-focused:not(.o-isDeviceSmall) {
|
||||
box-shadow: -5px -5px 10px rgba(#000000, 0.18);
|
||||
}
|
||||
|
||||
.o_Composer {
|
||||
border: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.o_ChatWindow_newMessageFormInput {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.o_ChatWindow_thread .o_ThreadView_messageList {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="mail.ChatWindow" owl="1">
|
||||
<t t-if="chatWindow">
|
||||
<div class="o_ChatWindow position-absolute bottom-0 d-flex flex-column rounded-top-3 bg-view" t-attf-class="{{ className }}" tabindex="0" t-att-data-visible-index="chatWindow.visibleIndex"
|
||||
t-att-class="{
|
||||
'o-focused': chatWindow.isFocused,
|
||||
'o-folded': chatWindow.isFolded,
|
||||
'o-fullscreen w-100 h-100': chatWindow.isFullscreen,
|
||||
'o-isDeviceSmall position-fixed': messaging.device.isSmall,
|
||||
'mw-100 mh-100': !messaging.device.isSmall,
|
||||
'o-new-message': !chatWindow.thread,
|
||||
}" t-att-style="chatWindow.componentStyle" t-on-keydown="chatWindow.onKeydown" t-on-focusout="chatWindow.onFocusout" t-att-data-chat-window-local-id="chatWindow.localId" t-att-data-thread-id="chatWindow.thread ? chatWindow.thread.id : ''" t-ref="root"
|
||||
t-att-data-thread-model="chatWindow.thread ? chatWindow.thread.model : ''"
|
||||
>
|
||||
<ChatWindowHeader
|
||||
className="'o_ChatWindow_header flex-grow-0 flex-shrink-0'"
|
||||
chatWindow="chatWindow"
|
||||
record="chatWindow.chatWindowHeaderView"
|
||||
/>
|
||||
<t t-if="chatWindow.channelMemberListView">
|
||||
<ChannelMemberList record="chatWindow.channelMemberListView" className="'bg-view'"/>
|
||||
</t>
|
||||
<t t-if="chatWindow.callSettingsMenu">
|
||||
<CallSettingsMenu record="chatWindow.callSettingsMenu" className="'bg-view'"/>
|
||||
</t>
|
||||
<t t-if="chatWindow.channelInvitationForm">
|
||||
<ChannelInvitationForm className="'o_ChatWindow_channelInvitationForm'" record="chatWindow.channelInvitationForm"/>
|
||||
</t>
|
||||
<t t-if="chatWindow.threadView">
|
||||
<ThreadView
|
||||
className="'o_ChatWindow_thread flex-grow-1 flex-shrink-1'"
|
||||
record="chatWindow.threadView"
|
||||
/>
|
||||
</t>
|
||||
<t t-if="chatWindow.newMessageAutocompleteInputView">
|
||||
<div class="o_ChatWindow_newMessageForm d-flex align-items-center m-3">
|
||||
<span class="o_ChatWindow_newMessageFormLabel flex-grow-0 flex-shrink-0 me-2">
|
||||
To:
|
||||
</span>
|
||||
<AutocompleteInputView
|
||||
className="'o_ChatWindow_newMessageFormInput flex-grow-1 flex-shrink-1 border'"
|
||||
record="chatWindow.newMessageAutocompleteInputView"
|
||||
/>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
// = ChatWindow Header View
|
||||
// ============================================================================
|
||||
// No CSS hacks, variables overrides only
|
||||
|
||||
.o_ChatWindowHeader {
|
||||
--ChatWindowHeader-background-color: #{$o-gray-100};
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { registerMessagingComponent } from '@mail/utils/messaging_component';
|
||||
|
||||
const { Component } = owl;
|
||||
|
||||
export class ChatWindowHeader extends Component {
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Public
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @returns {ChatWindow}
|
||||
*/
|
||||
get chatWindow() {
|
||||
return this.props.chatWindow;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {ChatWindowHeaderView}
|
||||
*/
|
||||
get chatWindowHeaderView() {
|
||||
return this.props.record;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Object.assign(ChatWindowHeader, {
|
||||
props: {
|
||||
chatWindow: Object,
|
||||
record: Object,
|
||||
},
|
||||
template: 'mail.ChatWindowHeader',
|
||||
});
|
||||
|
||||
registerMessagingComponent(ChatWindowHeader);
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue