Skip to content
13 changes: 4 additions & 9 deletions app/internal_packages/message-list/lib/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,15 @@ import {

import { MessageListHiddenMessagesToggle } from './message-list-hidden-messages-toggle';
import MessageList from './message-list';
import { SidebarPluginContainer } from './sidebar-plugin-container';
import { SidebarParticipantPicker } from './sidebar-participant-picker';
import { SidebarViewSwitcher } from './sidebar-view-switcher';

export function activate() {
if (AppEnv.isMainWindow()) {
// Register Message List Actions we provide globally
ComponentRegistry.register(MessageList, {
location: WorkspaceStore.Location.MessageList,
});
ComponentRegistry.register(SidebarParticipantPicker, {
location: WorkspaceStore.Location.MessageListSidebar,
});
ComponentRegistry.register(SidebarPluginContainer, {
ComponentRegistry.register(SidebarViewSwitcher, {
location: WorkspaceStore.Location.MessageListSidebar,
});
ComponentRegistry.register(MessageListHiddenMessagesToggle, {
Expand All @@ -33,7 +29,7 @@ export function activate() {
ComponentRegistry.register(MessageList, { location: WorkspaceStore.Location.Center });

// We need to locate the thread and focus it so that the MessageList displays it
DatabaseStore.find<Thread>(Thread, threadId).then(thread =>
DatabaseStore.find<Thread>(Thread, threadId).then((thread) =>
Actions.setFocus({ collection: 'thread', item: thread })
);

Expand All @@ -48,6 +44,5 @@ export function activate() {

export function deactivate() {
ComponentRegistry.unregister(MessageList);
ComponentRegistry.unregister(SidebarPluginContainer);
ComponentRegistry.unregister(SidebarParticipantPicker);
ComponentRegistry.unregister(SidebarViewSwitcher);
}
125 changes: 125 additions & 0 deletions app/internal_packages/message-list/lib/sidebar-agenda-view.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import moment, { Moment } from 'moment';
import React from 'react';
import { Rx, DatabaseStore, Calendar, Actions, localized } from 'mailspring-exports';
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unused imports: localized is imported here but never used in this file. Also CalendarView (line 10) is imported but unused.

Suggested change
import { Rx, DatabaseStore, Calendar, Actions, localized } from 'mailspring-exports';
import { Rx, DatabaseStore, Calendar, Actions } from 'mailspring-exports';

import {
CalendarDataSource,
EventOccurrence,
FocusedEventInfo,
} from '../../main-calendar/lib/core/calendar-data-source';
import { AgendaView } from '../../main-calendar/lib/core/agenda-view';
import { CalendarView } from '../../main-calendar/lib/core/calendar-constants';
import { CalendarEventPopover } from '../../main-calendar/lib/core/calendar-event-popover';
import { Disposable } from 'rx-core';

const DISABLED_CALENDARS = 'mailspring.disabledCalendars';

interface SidebarAgendaViewState {
focusedMoment: Moment;
selectedEvents: EventOccurrence[];
focusedEvent: FocusedEventInfo | null;
disabledCalendars: string[];
calendars: Calendar[];
}

export class SidebarAgendaView extends React.Component<{}, SidebarAgendaViewState> {
static displayName = 'SidebarAgendaView';

_dataSource = new CalendarDataSource();
_configDisposable?: Disposable;
_calDisposable?: Disposable;

constructor(props: {}) {
super(props);
this.state = {
focusedMoment: moment(),
selectedEvents: [],
focusedEvent: null,
disabledCalendars: AppEnv.config.get(DISABLED_CALENDARS) || [],
calendars: [],
};
}

componentDidMount() {
this._configDisposable = Rx.Observable.fromConfig<string[] | undefined>(
DISABLED_CALENDARS
).subscribe((disabledCalendars) => {
this.setState({ disabledCalendars: disabledCalendars || [] });
});

const calQuery = DatabaseStore.findAll<Calendar>(Calendar);
this._calDisposable = Rx.Observable.fromQuery(calQuery).subscribe((calendars) => {
this.setState({ calendars });
});
}

componentWillUnmount() {
this._configDisposable?.dispose();
this._calDisposable?.dispose();
}

_getReadOnlyCalendarIds(): Set<string> {
const ids = new Set<string>();
for (const cal of this.state.calendars) {
if (cal.readOnly) {
ids.add(cal.id);
}
}
return ids;
}

_onChangeFocusedMoment = (m: Moment) => {
this.setState({ focusedMoment: m, focusedEvent: null });
};

_onChangeView = () => {
// No-op in the sidebar — we always show the agenda view
};

_onEventClick = (e: React.MouseEvent, event: EventOccurrence) => {
this.setState({ selectedEvents: [event], focusedEvent: null });
};

_onEventDoubleClick = (event: EventOccurrence) => {
const el = document.getElementById(event.id);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dead code with a latent bug: AgendaView handles double-click internally via _onAgendaEventDoubleClick (line 184 of agenda-view.tsx) using e.currentTarget, and never calls this.props.onEventDoubleClick. So this handler is never invoked.

Additionally, if it were called, document.getElementById(event.id) would always return null because AgendaView renders event elements with day-unique IDs: id={\${event.id}-${dayKey}`}. The _onEventFocused` handler (line 93) delegates here, so it has the same issue.

Consider removing these handlers and passing no-ops, or fixing the lookup to match the actual DOM IDs.

if (!el) return;
Actions.openPopover(<CalendarEventPopover event={event} />, {
originRect: el.getBoundingClientRect(),
direction: 'left',
fallbackDirection: 'right',
closeOnAppBlur: false,
});
};

_onEventFocused = (event: EventOccurrence) => {
this._onEventDoubleClick(event);
};

// No-ops for drag and mouse handlers — not needed in sidebar
_noopMouseHandler = () => {};
_noopDragStart = () => {};

render() {
return (
<div className="sidebar-agenda-view">
<AgendaView
dataSource={this._dataSource}
disabledCalendars={this.state.disabledCalendars}
focusedMoment={this.state.focusedMoment}
focusedEvent={this.state.focusedEvent}
selectedEvents={this.state.selectedEvents}
onChangeView={this._onChangeView}
onChangeFocusedMoment={this._onChangeFocusedMoment}
onEventClick={this._onEventClick}
onEventDoubleClick={this._onEventDoubleClick}
onEventFocused={this._onEventFocused}
onCalendarMouseUp={this._noopMouseHandler}
onCalendarMouseDown={this._noopMouseHandler}
onCalendarMouseMove={this._noopMouseHandler}
dragState={null}
onEventDragStart={this._noopDragStart}
readOnlyCalendarIds={this._getReadOnlyCalendarIds()}
/>
</div>
);
}
}
109 changes: 109 additions & 0 deletions app/internal_packages/message-list/lib/sidebar-view-switcher.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import React from 'react';
import { PropTypes, FocusedContactsStore, Contact } from 'mailspring-exports';
import { InjectedComponentSet } from 'mailspring-component-kit';
import { SidebarViewToggle, SidebarView } from './sidebar-view-toggle';
import { SidebarParticipantPicker } from './sidebar-participant-picker';
import { SidebarAgendaView } from './sidebar-agenda-view';

class FocusedContactStorePropsContainer extends React.Component<
{ children: React.ReactElement<any> },
{ sortedContacts: Contact[]; focusedContact: Contact }
> {
static displayName = 'FocusedContactStorePropsContainer';

unsubscribe: () => void;

constructor(props) {
super(props);
this.state = this._getStateFromStores();
}

componentDidMount() {
this.unsubscribe = FocusedContactsStore.listen(this._onChange);
}

componentWillUnmount() {
this.unsubscribe();
}

_onChange = () => {
this.setState(this._getStateFromStores());
};

_getStateFromStores() {
return {
sortedContacts: FocusedContactsStore.sortedContacts(),
focusedContact: FocusedContactsStore.focusedContact(),
};
}

render() {
let classname = 'sidebar-section';
let inner = null;
if (this.state.focusedContact) {
classname += ' visible';
inner = React.cloneElement(this.props.children, this.state);
}
return <div className={classname}>{inner}</div>;
}
}

const SidebarPluginContainerInner = (props) => {
return (
<InjectedComponentSet
className="sidebar-contact-card"
key={props.focusedContact.email}
matching={{ role: 'MessageListSidebar:ContactCard' }}
direction="column"
exposedProps={{
contact: props.focusedContact,
}}
/>
);
};

SidebarPluginContainerInner.propTypes = {
focusedContact: PropTypes.object,
};

export class SidebarViewSwitcher extends React.Component<{}, { selectedView: SidebarView }> {
static displayName = 'SidebarViewSwitcher';

static containerStyles = {
order: 0,
flexShrink: 0,
minWidth: 150,
maxWidth: 300,
};

constructor(props: {}) {
super(props);
this.state = {
selectedView: 'participants',
};
}

_onSelectView = (view: SidebarView) => {
this.setState({ selectedView: view });
};

render() {
const { selectedView } = this.state;

return (
<div className="sidebar-view-switcher">
<SidebarViewToggle selectedView={selectedView} onSelectView={this._onSelectView} />
{selectedView === 'participants' ? (
<div className="sidebar-participants-view">
<SidebarParticipantPicker />
<FocusedContactStorePropsContainer>
<SidebarPluginContainerInner />
</FocusedContactStorePropsContainer>
</div>
) : (
<SidebarAgendaView />
)}
</div>
);
}
}
33 changes: 33 additions & 0 deletions app/internal_packages/message-list/lib/sidebar-view-toggle.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import React from 'react';
import { localized } from 'mailspring-exports';

export type SidebarView = 'participants' | 'calendar';

interface SidebarViewToggleProps {
selectedView: SidebarView;
onSelectView: (view: SidebarView) => void;
}

export class SidebarViewToggle extends React.Component<SidebarViewToggleProps> {
static displayName = 'SidebarViewToggle';

render() {
const { selectedView, onSelectView } = this.props;
return (
<div className="sidebar-view-toggle">
<button
className={`toggle-btn ${selectedView === 'participants' ? 'active' : ''}`}
onClick={() => onSelectView('participants')}
>
{localized('Participants')}
</button>
<button
className={`toggle-btn ${selectedView === 'calendar' ? 'active' : ''}`}
onClick={() => onSelectView('calendar')}
>
{localized('Calendar')}
</button>
</div>
);
}
}
Loading