Skip to content

Commit

Permalink
Fix missing unread badge (#908)
Browse files Browse the repository at this point in the history
  • Loading branch information
pkuczynski authored and sindresorhus committed May 30, 2019
1 parent 15fc43b commit 16d9e3b
Show file tree
Hide file tree
Showing 3 changed files with 172 additions and 124 deletions.
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'
};

0 comments on commit 16d9e3b

Please sign in to comment.