Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix missing unread badge #908

Merged
merged 4 commits into from
May 30, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
132 changes: 8 additions & 124 deletions source/browser.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import {ipcRenderer as ipc, Event as ElectronEvent} from 'electron';
import elementReady from 'element-ready';
import {api, is} from 'electron-util';

import selectors from './browser/selectors';
import config from './config';

const listSelector = 'div[role="navigation"] > div > ul';
import './browser/conversation-list'; // eslint-disable-line import/no-unassigned-import

const conversationSelector = '._4u-c._1wfr > ._5f0v.uiScrollableArea';
const selectedConversationSelector = '._5l-3._1ht1._1ht2';
const preferencesSelector = '._10._4ebx.uiLayer._4-hy';
Expand Down Expand Up @@ -337,7 +340,7 @@ async function jumpToConversation(key: number): Promise<void> {

// Focus on the conversation with the given index
async function selectConversation(index: number): Promise<void> {
const conversationElement = (await elementReady(listSelector)).children[index];
const conversationElement = (await elementReady(selectors.conversationList)).children[index];

if (conversationElement) {
(conversationElement.firstChild!.firstChild as HTMLElement).click();
Expand Down Expand Up @@ -416,112 +419,6 @@ function closePreferences(): void {
doneButton.click();
}

/// async function sendConversationList(): Promise<void> {
// const conversations: Conversation[] = await Promise.all(
// ([...(await elementReady(listSelector)).children] as HTMLElement[])
// .splice(0, 10)
// .map(async (el: HTMLElement) => {
// const profilePic = el.querySelector<HTMLImageElement>('._7t0d img')!;

// // TODO: This logic is broken in the latest Messenger update.
// // const groupPic = el.querySelector<HTMLImageElement>('._7q1j div');

// // // This is only for group chats
// // if (groupPic) {
// // // Slice image source from background-image style property of div
// // const bgImage = groupPic.style.backgroundImage!;
// // groupPic.src = bgImage.slice(5, bgImage.length - 2);
// // }

// const isConversationMuted = el.classList.contains('_569x');

// return {
// label: el.querySelector<HTMLElement>('._1ht6')!.textContent!,
// selected: el.classList.contains('_1ht2'),
// unread: el.classList.contains('_1ht3') && !isConversationMuted,
// icon: await getDataUrlFromImg(
// profilePic,
// el.classList.contains('_1ht3')
// )
// };
// })
// );

// ipc.send('conversations', conversations);
// }

// Return canvas with rounded image
// async function urlToCanvas(url: string, size: number): Promise<HTMLCanvasElement> {
// return new Promise(resolve => {
// const img = new Image();
// img.crossOrigin = 'anonymous';
// img.addEventListener('load', () => {
// const canvas = document.createElement('canvas');
// const padding = {
// top: 3,
// right: 0,
// bottom: 3,
// left: 0
// };

// canvas.width = size + padding.left + padding.right;
// canvas.height = size + padding.top + padding.bottom;

// const ctx = canvas.getContext('2d')!;
// ctx.save();
// ctx.beginPath();
// ctx.arc(size / 2 + padding.left, size / 2 + padding.top, size / 2, 0, Math.PI * 2, true);
// ctx.closePath();
// ctx.clip();
// ctx.drawImage(img, padding.left, padding.top, size, size);
// ctx.restore();

// resolve(canvas);
// });

// img.src = url;
// });
// }

// Return data url for user avatar
// async function getDataUrlFromImg(img: HTMLImageElement, unread: boolean): Promise<string> {
// // eslint-disable-next-line no-async-promise-executor
// return new Promise(async resolve => {
// if (unread) {
// const dataUnreadUrl = img.getAttribute('dataUnreadUrl');

// if (dataUnreadUrl) {
// return resolve(dataUnreadUrl);
// }
// } else {
// const dataUrl = img.getAttribute('dataUrl');

// if (dataUrl) {
// return resolve(dataUrl);
// }
// }

// const canvas = await urlToCanvas(img.src, 30);
// const ctx = canvas.getContext('2d')!;
// const dataUrl = canvas.toDataURL();
// img.setAttribute('dataUrl', dataUrl);

// if (!unread) {
// return resolve(dataUrl);
// }

// const markerSize = 8;
// ctx.fillStyle = '#f42020';
// ctx.beginPath();
// ctx.ellipse(canvas.width - markerSize, markerSize, markerSize, markerSize, 0, 0, 2 * Math.PI);
// ctx.fill();
// const dataUnreadUrl = canvas.toDataURL();
// img.setAttribute('dataUnreadUrl', dataUnreadUrl);

// resolve(dataUnreadUrl);
// });
// }

// Inject a global style node to maintain custom appearance after conversation change or startup
document.addEventListener('DOMContentLoaded', () => {
const style = document.createElement('style');
Expand Down Expand Up @@ -549,21 +446,6 @@ document.addEventListener('DOMContentLoaded', () => {
});

window.addEventListener('load', () => {
// TODO: Broken in the latest Messenger update
// const sidebar = document.querySelector<HTMLElement>('[role=navigation]');

// if (sidebar) {
// sendConversationList();

// const conversationListObserver = new MutationObserver(sendConversationList);
// conversationListObserver.observe(sidebar, {
// subtree: true,
// childList: true,
// attributes: true,
// attributeFilter: ['class']
// });
// }

if (location.pathname.startsWith('/login')) {
const keepMeSignedInCheckbox = document.querySelector<HTMLInputElement>('#u_0_0')!;
keepMeSignedInCheckbox.checked = config.get('keepMeSignedIn');
Expand All @@ -578,7 +460,9 @@ window.addEventListener('blur', () => {
document.documentElement.classList.add('is-window-inactive');
});
window.addEventListener('focus', () => {
document.documentElement.classList.remove('is-window-inactive');
if (document.documentElement) {
document.documentElement.classList.remove('is-window-inactive');
}
});

// It's not possible to add multiple accelerators
Expand Down
161 changes: 161 additions & 0 deletions source/browser/conversation-list.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
import {ipcRenderer as ipc} from 'electron';
import elementReady from 'element-ready';

import selectors from './selectors';

const icon = {
read: 'data-caprine-icon',
unread: 'data-caprine-icon-unread'
};

const padding = {
top: 3,
right: 0,
bottom: 3,
left: 0
};

function drawIcon(size: number, img?: HTMLImageElement): HTMLCanvasElement {
const canvas = document.createElement('canvas');

if (img) {
canvas.width = size + padding.left + padding.right;
canvas.height = size + padding.top + padding.bottom;

const ctx = canvas.getContext('2d')!;
ctx.save();
ctx.beginPath();
ctx.arc(size / 2 + padding.left, size / 2 + padding.top, size / 2, 0, Math.PI * 2, true);
ctx.closePath();
ctx.clip();

ctx.drawImage(img, padding.left, padding.top, size, size);
ctx.restore();
} else {
canvas.width = 0;
canvas.height = 0;
}

return canvas;
}

// Return canvas with rounded image
async function urlToCanvas(url: string, size: number): Promise<HTMLCanvasElement> {
return new Promise(resolve => {
const img = new Image();

img.crossOrigin = 'anonymous';

img.addEventListener('load', () => {
resolve(drawIcon(size, img));
});

img.addEventListener('error', () => {
console.error('Image not found', url);
resolve(drawIcon(size));
});

img.src = url;
});
}

async function createIcons(el: HTMLElement, url: string): Promise<void> {
const canvas = await urlToCanvas(url, 50);

el.setAttribute(icon.read, canvas.toDataURL());

const markerSize = 8;
const ctx = canvas.getContext('2d')!;

ctx.fillStyle = '#f42020';
ctx.beginPath();
ctx.ellipse(canvas.width - markerSize, markerSize, markerSize, markerSize, 0, 0, 2 * Math.PI);
ctx.fill();

el.setAttribute(icon.unread, canvas.toDataURL());
}

async function discoverIcons(el: HTMLElement): Promise<void> {
const profilePicElement = el.querySelector<HTMLImageElement>('img:first-of-type');

if (profilePicElement) {
return createIcons(el, profilePicElement.src);
}

const groupPicElement = el.firstElementChild as HTMLElement;

if (groupPicElement) {
const groupPicBackground = groupPicElement.style.backgroundImage;

if (groupPicBackground) {
return createIcons(el, groupPicBackground.replace(/^url\(["']?(.*?)["']?\)$/, '$1'));
}
}

console.warn('Could not discover profile picture. Falling back to default image.');

// Fall back to messenger favicon
const messengerIcon = document.querySelector('link[rel~="icon"]');

if (messengerIcon) {
return createIcons(el, messengerIcon.getAttribute('href')!);
}

// Fall back to facebook favicon
return createIcons(el, 'https://facebook.com/favicon.ico');
}

async function getIcon(el: HTMLElement, unread: boolean): Promise<string> {
if (!el.getAttribute(icon.read)) {
await discoverIcons(el);
}

return el.getAttribute(unread ? icon.unread : icon.read)!;
}

async function createConversation(el: HTMLElement): Promise<Conversation> {
const conversation: Partial<Conversation> = {};
const muted = el.classList.contains('_569x');

conversation.selected = el.classList.contains('_1ht2');
conversation.unread = !muted && el.getAttribute('aria-live') !== null;

const profileElement = el.querySelector<HTMLElement>('div[data-tooltip-content]')!;

conversation.label = profileElement.getAttribute('data-tooltip-content')!;
conversation.icon = await getIcon(profileElement, conversation.unread);

return conversation as Conversation;
}

async function createConversationList(): Promise<Conversation[]> {
const list: HTMLElement = await elementReady(selectors.conversationList);
const items: HTMLElement[] = [...list.children] as HTMLElement[];

const conversations: Conversation[] = await Promise.all(
items.map((el: HTMLElement): Promise<Conversation> => createConversation(el))
);

return conversations;
}

async function sendConversationList(): Promise<void> {
ipc.send('conversations', await createConversationList());
}

window.addEventListener('load', () => {
const sidebar = document.querySelector<HTMLElement>('[role=navigation]');

if (sidebar) {
sendConversationList();

const conversationListObserver = new MutationObserver(sendConversationList);

conversationListObserver.observe(sidebar, {
subtree: true,
childList: true,
attributes: true,
attributeFilter: ['class']
});
}
});
3 changes: 3 additions & 0 deletions source/browser/selectors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default {
conversationList: 'div[role="navigation"] > div > ul'
};