Initial commit: Core packages

This commit is contained in:
Ernad Husremovic 2025-08-29 15:20:45 +02:00
commit 12c29a983b
9512 changed files with 8379910 additions and 0 deletions

View file

@ -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);

View file

@ -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;
}

View file

@ -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>

View file

@ -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);

View file

@ -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;
}

View file

@ -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>

View file

@ -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);

View file

@ -0,0 +1,7 @@
// ------------------------------------------------------------------
// Layout
// ------------------------------------------------------------------
.o_ActivityMarkDonePopoverContent_feedback {
min-height: 70px;
}

View file

@ -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 &amp; 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>

View file

@ -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',
});

View file

@ -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>

View file

@ -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);

View file

@ -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;
}

View file

@ -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>

View file

@ -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);

View file

@ -0,0 +1,7 @@
// ------------------------------------------------------------------
// Style
// ------------------------------------------------------------------
.o_AttachmentBox_dashedLine {
border-top: $border-width dashed $border-color;
}

View file

@ -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>

View file

@ -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);

View file

@ -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;
}

View file

@ -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>

View file

@ -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);

View file

@ -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>

View file

@ -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);

View file

@ -0,0 +1,19 @@
// ------------------------------------------------------------------
// Layout
// ------------------------------------------------------------------
.o_AttachmentImage {
min-width: 20px;
min-height: 20px;
img {
object-fit: contain;
}
}
// ------------------------------------------------------------------
// Style
// ------------------------------------------------------------------
.o_AttachmentImage {
cursor: zoom-in;
}

View file

@ -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>

View file

@ -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);

View file

@ -0,0 +1,3 @@
.o_AttachmentList {
max-width: var(--Chatter-max-width, none);
}

View file

@ -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>

View file

@ -0,0 +1,7 @@
// = Attachment Viewer View
// ============================================================================
// No CSS hacks, variables overrides only
.o_AttachmentViewer_toolbarButton {
--AttachmentViewer_toolbarButton-background-color: #{$o-gray-200};
}

View file

@ -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);

View file

@ -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;
}
}

View file

@ -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>

View file

@ -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);

View file

@ -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>

View file

@ -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};
}

View file

@ -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);

View file

@ -0,0 +1,4 @@
.o_CallActionList_button:not(.btn-danger) {
background-color: var(--CallActionList_button-background-color, #{$o-gray-800});
color: #FFFFFF;
}

View file

@ -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>

View file

@ -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);

View file

@ -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;
}

View file

@ -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>

View file

@ -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);

View file

@ -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;
}

View file

@ -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>

View file

@ -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);

View file

@ -0,0 +1,7 @@
// ------------------------------------------------------------------
// Layout
// ------------------------------------------------------------------
.o_CallInviteRequestPopupList {
z-index: $zindex-modal;
}

View file

@ -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>

View file

@ -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);

View file

@ -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%);
}

View file

@ -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>

View file

@ -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);

View file

@ -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;
}
}

View file

@ -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>

View file

@ -0,0 +1,7 @@
// = Call Participant Card View
// ============================================================================
// No CSS hacks, variables overrides only
.o_CallParticipantCard_avatarFrame {
--CallParticipantCard_avatarFrame-background-color: #{$o-gray-100};
}

View file

@ -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);

View file

@ -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});
}

View file

@ -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>

View file

@ -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);

View file

@ -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>

View file

@ -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);

View file

@ -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>

View file

@ -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);

View file

@ -0,0 +1,7 @@
// ------------------------------------------------------------------
// Layout
// ------------------------------------------------------------------
.o_CallSettingsMenu_optionInputGroupInput {
flex-grow: 2;
}

View file

@ -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>

View file

@ -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);

View file

@ -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>

View file

@ -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);

View file

@ -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');
}
}

View file

@ -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>

View file

@ -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);

View file

@ -0,0 +1,11 @@
// ------------------------------------------------------------------
// Layout
// ------------------------------------------------------------------
.o_CallSystrayMenu_buttonContent {
max-width: 150px;
}
.o_CallSystrayMenu_dot {
animation: flash 3s ease infinite;
}

View file

@ -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>

View file

@ -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',
});

View file

@ -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>

View file

@ -0,0 +1,7 @@
// = Call View
// ============================================================================
// No CSS hacks, variables overrides only
.o_CallView {
--CallView-background-color: #{$o-gray-100};
}

View file

@ -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);

View file

@ -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)"};
}
}

View file

@ -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>

View file

@ -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);

View file

@ -0,0 +1,7 @@
// ------------------------------------------------------------------
// Layout
// ------------------------------------------------------------------
.o_ChannelInvitationForm_selectedPartners {
max-height: 100px;
}

View file

@ -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>

View file

@ -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);

View file

@ -0,0 +1,14 @@
.o_ChannelInvitationFormSelectablePartner_avatar {
object-fit: cover;
}
.o_ChannelInvitationFormSelectablePartner_avatarContainer {
width: 32px;
height: 32px;
}
.o_ChannelInvitationForm_selectablePartnerName {
min-width: 0;
}

View file

@ -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>

View file

@ -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);

View file

@ -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>

View file

@ -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);

View file

@ -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;
}

View file

@ -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>

View file

@ -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);

View file

@ -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>

View file

@ -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);

View file

@ -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>

View file

@ -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);

View file

@ -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>

View file

@ -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);

View file

@ -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;
}

View file

@ -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>

View file

@ -0,0 +1,7 @@
// = ChatWindow Header View
// ============================================================================
// No CSS hacks, variables overrides only
.o_ChatWindowHeader {
--ChatWindowHeader-background-color: #{$o-gray-100};
}

View file

@ -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