Conversation
Wondering if we should enable the valid-jsdoc eslint rule... |
@montehurd, yes! Please enable valid-jsdoc. I'm sorry but I thought we already had it enabled and was mistaken. |
src/transform/FooterContainer.css
Outdated
@@ -0,0 +1,25 @@ | |||
|
|||
.footer_container { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's fine to punt on this but consider using a common prefix like pagelib-footer-thing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good idea! Once I get Read more
into this PR I'll make a wholesale change.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed! (I didn't change from underscores to dashes yet though)
src/transform/FooterContainer.js
Outdated
const elements = document.querySelectorAll( | ||
'#footer_container_menu_heading, #footer_container_readmore, #footer_container_legal' | ||
) | ||
Array.from(elements) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see you already called this out in the todo but consider replacing Array.from()
with Array.prototype.slice.call()
for old time Android devices.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed with polyfill.
src/transform/FooterMenu.css
Outdated
} | ||
|
||
.footer_menu_icon_disambiguation { | ||
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFAAAABQCAYAAACOEfKtAAAAAXNSR0IArs4c6QAAAWRJREFUeAHt1ruOwjAQBVAW8YnbbMHXUdDwj6xWKyigGJLrcaTo0PBwBifH1xofDl4ECBAgQIAAAQIECBAgQIAAAQIECBAgQGCGwNeMSf7m+P4532fNtWSe2/USGRyXTObad4HT+0+9v6QrPuruRu0ICQxXBCDAUCAsl0CAoUBYPqwLf9rVPr0ufK5neXfXt4Wf1Os+DEvgY/ruFX/MU73PSroEVitRjAMsgKphgJVQMQ6wAKqGAVZCxTjAAqgaBlgJFePDz4Gv8806j211/pTA1xVf+L09gVslY6HD6sslcDXdfyFAgKFAWC6BAEOBsLy9CzsHhiu09/L2BDoH7j1C4fPpwgBDgbBcAgGGAmF5exd2DgxXaO/l7Qnc+hzYvQM0kXCLtCcwvL+4vHsHSGC4RAABhgJhuQQCDAXC8uFduPvcFT7v8HJbeDipPyRAgAABAgQIECBAgAABAgQIECBAYK8Cv3qKKmmwPBETAAAAAElFTkSuQmCC); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder why GitHub is coloring this line differently.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ha I was wondering about this too!
* Extracts array of no-html page issues strings from document. | ||
* @type {FooterMenuItemPayloadExtractor} | ||
*/ | ||
const pageIssuesStringsArray = document => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems to be a method but uses @type
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See the FooterMenuItemPayloadExtractor
typedef for the details. I think I did this correctly according to the Type definitions
box in the docs. Lemme know if not :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍 this looks good! My mistake!
src/transform/FooterMenu.js
Outdated
payloadExtractor() { | ||
switch (this.itemType) { | ||
case MenuItemType.languages: | ||
return null |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please use undefined
instead of null
per earlier discussion.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed!
src/transform/FooterMenu.js
Outdated
item.classList.add(iconClass) | ||
} | ||
|
||
return document.createDocumentFragment().appendChild(item) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it intentional to a return a value from the constructor?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed! (Fragment makers can just be functions.)
src/transform/index.js
Outdated
@@ -9,6 +12,9 @@ import WidenImage from './WidenImage' | |||
|
|||
export default { | |||
CollapseTable, | |||
FooterContainer, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is great how you have these broken out!
src/transform/FooterLegal.css
Outdated
background-position: right top; | ||
} | ||
|
||
.footer_legal_licence { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Typo!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed.
src/transform/FooterContainer.css
Outdated
} | ||
|
||
#pagelib_footer_container_readmore_pages::-webkit-scrollbar { | ||
display: none; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm sure you tried this already but does overflow: hidden
not work?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I removed this - was unused.
src/transform/FooterContainer.js
Outdated
* @return {void} | ||
*/ | ||
const updateBottomPaddingToAllowReadMoreToScrollToTop = (document, window) => { | ||
const div = document.getElementById('pagelib_footer_container_ensure_can_scroll_to_top') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider making a symbol for this ID.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok to hold off on this for now? The template complicates doing this... it's filled with such ids...
src/transform/FooterContainer.js
Outdated
|
||
/** | ||
* Ensures the 'Read more' section header can always be scrolled to the top of the screen. | ||
* @param {!Document} document |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we want to have a document parameter or can we just use window.document
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed!
src/transform/FooterContainer.js
Outdated
const updateBottomPaddingToAllowReadMoreToScrollToTop = (document, window) => { | ||
const div = document.getElementById('pagelib_footer_container_ensure_can_scroll_to_top') | ||
let currentPadding = parseInt(div.style.paddingBottom, 10) | ||
if (isNaN(currentPadding)) { currentPadding = 0 } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since NaN is falsy, I think the preceeding line could just be currentPadding = parseInt(...) || 0
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed!
src/transform/FooterContainer.js
Outdated
const div = document.getElementById('pagelib_footer_container_ensure_can_scroll_to_top') | ||
let currentPadding = parseInt(div.style.paddingBottom, 10) | ||
if (isNaN(currentPadding)) { currentPadding = 0 } | ||
const height = div.clientHeight - currentPadding |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need to check border width and margin here too?
src/transform/FooterMenu.js
Outdated
fragment.appendChild(tables[i].cloneNode(true)) | ||
} | ||
// Remove some element so their text doesn't appear when we use "innerText" | ||
Array.from(fragment.querySelectorAll('.hide-when-compact, .collapsed')).forEach(el => el.remove()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since I ended up with a lot of these in my own code, I ended up adding a polyfill in my patch for querySelectorAll.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I copied your polyfill method over and used it :)
src/transform/FooterMenu.js
Outdated
|
||
/** | ||
* Type representing kinds of menu items. | ||
* @type {Object} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm still fuzzy on how these @type
/ @typedef
are intended to work but I think this should be @type {!MenuItemType}
and in another comment you'd do @typedef {{name: !string, id: !number}} MenuItemType
or
@typedef {object} MenuItemType
@var {!number} languages
@var {!number} lastEdited
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How about this? Fixed if that seems ok.
* @return {!string} | ||
*/ | ||
iconClass() { | ||
switch (this.itemType) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
An alternative to this switch might be:
class MenuItemType {
constructor(id, iconClass) {
this._id = id
this._iconClass = iconClass
}
get id() { return this._id }
get iconClass() { return this._iconClass }
}
const MENU_ITEMS = {
LANGUAGES: new MenuItemType(1, 'pagelib-footer-menu-icon-languages'),
LAST_EDITED: new MenuItemType(2, 'pagelib-footer-menu-icon-last-edited'),
PAGE_ISSUES: new MenuItemType(3, 'pagelib-footer-menu-icon-page-issues'),
DISAMBIGUATION: new MenuItemType(4, 'pagelib-footer-menu-icon-disambiguation'),
COORDINATE: new MenuItemType(5, 'pagelib-footer-menu-icon-coordinate')
}
Then the WMFMenuItem.iconClass()
implementation becomes return menuItem.iconClass()
and you don't have to remember to update it when adding new types.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I chewed on this for a bit... thinking I'd prefer to keep MenuItemType
an enum. Also was curious as to the reason for opposition to switch... Thoughts?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The reason is the elimination of conditional logic and the potential to fall out of sync between the conditional and the declaration, adding new entries is one line instead of a couple, and everything that embodies a menu item's distinction from other items is in one place.
WRT the discussion on how payloadExtractor() would look after this change, you just tack on the ID getter on to the switch and cases (if it's not possible to also embed this conditional into MenuItemType itself):
payloadExtractor() {
switch (this.itemType.id) {
case MenuItemType.pageIssues.id: return pageIssuesStringsArray
case MenuItemType.disambiguation.id: return disambiguationTitlesArray
default: return undefined
}
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If it's ok I'd prefer to consider internals refactors in follow-ups. Preferably after I've had a chance to add a few more tests.
src/transform/FooterMenu.js
Outdated
/** | ||
* Menu item model. | ||
*/ | ||
class WMFMenuItem { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Everything in the library will probably be WMF-ish. Any reason not to call this just "MenuItem"?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed!
src/transform/FooterReadMore.js
Outdated
* @param {!string} headingID | ||
* @param {!Document} document | ||
*/ | ||
const setHeading = (headingString, headingID, document) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider consistently placing the document / window parameter in the same position in each method signature unless there's a good reason to deviate.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed!
src/transform/FooterReadMore.js
Outdated
* @return {void} | ||
*/ | ||
|
||
let _saveForLaterString = null |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These globals suggest that a class implementation may be more appropriate. The client can than keep one or more instances of that class as needed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good news! I didn't need those globals after all. We were already calling a method from native land to set the save button icon state - I modified it to be passed the text to use for the current state of the save button at the same time.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So...fixed!
src/transform/FooterMenu.css
Outdated
@@ -0,0 +1,63 @@ | |||
|
|||
.pagelib_footer_menu_item { | |||
padding: 10px; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would you mind if we stuck with two-space indentation for CSS too?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed!
src/transform/FooterMenu.js
Outdated
* @typedef {number} MenuItemType | ||
*/ | ||
|
||
// eslint-disable-next-line |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please limit this to the rules you want to disable. e.g., eslint-disable-next-line valid-jsdoc
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah will do.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed!
for (let i = 0; i < tables.length; i++) { | ||
fragment.appendChild(tables[i].cloneNode(true)) | ||
} | ||
// Remove some element so their text doesn't appear when we use "innerText" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because you're using a DocumentFragment, you have to manually walk clone and walk this list to remove hidden elements? Would using window.document
allow you to avoid the clone and selective deletion? According to the MDN documentation, Node.innerText
(as opposed to Node.textContent
): "innerText
is aware of style and will not return the text of hidden elements, whereas textContent
will."
https://developer.mozilla.org/en-US/docs/Web/API/Node/textContent
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
iirc I tried that. I'll add some tests with the tricky cases I found then we can safely try alternatives.
src/transform/FooterReadMore.css
Outdated
margin-bottom: 10px; | ||
border-radius: 2px; | ||
box-shadow: 0px 2px 4px rgba(200, 204, 209, 0.5); | ||
display:block; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing a space here between display
and block
:]
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed!
src/transform/FooterReadMore.css
Outdated
} | ||
|
||
.pagelib_footer_readmore_page_title { | ||
color: #222222; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would you mind if we used #rgb[a]
notation here? #222
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed!
src/transform/FooterReadMore.css
Outdated
} | ||
|
||
.pagelib_footer_readmore_bookmark_unfilled { | ||
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAABGCAYAAACKeRdbAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAAZiS0dEAP8A/wD/oL2nkwAABIdJREFUaN7tmltMVEcYgL+zV1AU5KJAFSkg64OtNd5SW3vRhFTTRGuTXrXxAktFRaWYNNqkapt0DaKlFaWUpNE2PtnENjZprS82JhqVJkRj3QVKIaAg5bqwN3b39OG4RwyiK7KMpud72pmdM/N//+yZM7M50lzrZZknGJ3oAP73AobBhcvfzBUdT1jMy69WPz/xM6AJiEYTEI0mIBpNQDSagGg0AdFoAqLRBESjCYhGExCNJiAaTUA0moBoNAHRaAKi0QREowmIRhMQjSYwHLIMtuNNfHasETmC7wIYHr2LoQSC8Ol3Dfx6sRMAjy/I3vVPo49AukZdwOeX+bjyb/6o6VbrfrvUiccXxGbNwGiQRnW8Uc2J2xuk8Kvau4IPcbamm+3ldXh8wcdTwOkKsPGgg8t2p1q3fnkK65alqOUL13op/LoOl3f0JEZFoNPpx1rq4GpDv1pX+OZUClaksmllKhtXPKXW/+lwUnDQgdMVeDwEWjt95JbYqW12KR3qJHauns4HOVPUNhuWJ1P01jS1fLWhH2upgy6nX6xAc7uX3BI7TW0eAAx6ib3r0lm1OHFI2/eWTmbX6unodMpNXNvsIm+/nfbuATEC9Tc85JbYae30AWAy6tiXn8lrC+KHveaNxYnsWZuO/rbEP60e8vbbudnhG1uB600u8kvt/NujZC/KpOPLzVm8PDv2gdcuWxjPF9YMDHpFQp3FW96xEaip7+fDAw66+5Tfb0y0nvJt2SyYOSHsPpbMiaO0IAuzURm+rctHXomduhZ3ZAUuXXeyuayWPreygsTFGKgoymZ25viHHviFWRMp25JFtFkPQEfvAPmlDv5qdEVG4NyVHrYeqsPtVYJPjDVSWWxhZtq4hw4+xDzLBA5vm0FMtCLR0+9n40EHNfX9YfcRlsCZ6i6Kj9TjG1AeQCkJZqp2WMhIiRpx8CGeyRhPRVE2cTHKrqbPHWBzWS0XrzvDuv6BAqfOd7CrqgF/QNlSpk2JomqHhalJ5kcOPsTMtHFUFltIjDUC4PYG2HaojnNXeh5N4MTZdvYcbSQQVILPTI3m22ILUyYZRy34EBkpSmKS400A+AaCFB+p50x118gEvj/dhu14E/LtzXwoSwkTI7IDB2Bqkvmu2fUHZHZVNXDqfMfDCVT9cpOyH5vV8rOZMVQUZRM7Xh+x4EMkx5uU+ys1GoBAUGbP0UZOnG0PT6D85A0qfr6hludZJlC+9c5KMRYkxhqp/ChbXeFkWcZ2vIkffm8b0lafOte6O1To9wQ5drpV/XLRrFgObMoiyjT2R+cok46c+fFUO/q41aVsNS5c60WSJKodd1Yoabi31195Lg7boEe+KNzeINvL6+46ZwzmnqnNmR/PvvxM4cEDRJt1lG2ZwaJZ995nDRF4/fkEPt8QmQP4SDEbJQ4UZPLqnEn3F1j1UhK716ajE5/4IRj0EjZrBssWJtxb4J0lk9n5fproOO+LXgd716Wz8sU7ByYdwJqcZIrfnjbijscSSYJP1kzn3aW3j6xHfmqRn1TKT7bIkixH8o+/yPMfGVAsFZFJcHoAAAAASUVORK5CYII=); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The indent is inconsistent here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed!
src/transform/FooterReadMore.css
Outdated
} | ||
|
||
.pagelib_footer_readmore_page_save { | ||
color: #2C5BC5; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One more lowercase case :]
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed!
src/transform/FooterReadMore.js
Outdated
* @return {void} | ||
*/ | ||
|
||
const _saveButtonIDPrefix = 'readmore:save:' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks like a static immutable constant, would you mind if we stuck with UPPER_CASE
-like syntax for these. So, SAVE_BUTTON_ID_PREFIX? I guess our functions are also constants but this is constant data at file scope.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not big on upper-casing WHYAREYOUSHOUTING but I like consistency so will do :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed!
src/transform/FooterReadMore.js
Outdated
if (readMorePage.terms) { | ||
description = readMorePage.terms.description[0] | ||
} | ||
if ((description === undefined || description.length < 10) && readMorePage.extract) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This could be concisely written as (description || '').length < 10
or (!description || description.length < 10)
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ooh nice! I'll try that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed!
src/transform/FooterReadMore.js
Outdated
} | ||
} | ||
} | ||
xhr.onerror = e => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The curly braces do not appear to add any value here and the parameter is unused.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed!
src/transform/FooterReadMore.js
Outdated
const unfilledClass = 'pagelib_footer_readmore_bookmark_unfilled' | ||
const filledClass = 'pagelib_footer_readmore_bookmark_filled' | ||
button.classList.remove(unfilledClass) | ||
button.classList.remove(filledClass) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since add won't doubly add, consider reducing this to: button.classList.remove(isSaved ? unfilledClass : filledClass)
.
https://developer.mozilla.org/en-US/docs/Web/API/Element/classList
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh sweet good catch!!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Turns out classList remove
can be passed multiple class names. I used that rather than adding another ternary.
👍 published as v4.3.0. Please update your integration patch to reference this version (and don't forget to unlink before building the bundle). |
hehe that's got me a few times :) |
https://phabricator.wikimedia.org/T164137
As these are consolidated I'm concurrently updating this iOS PR.
New transforms:
Misc:
Needed follow-ups:
Feedback comments which I've yet to address or which we haven't come to consensus on yet:
#53 (review)
#53 (review)
#53 (review)
#53 (review)
#53 (review)
#53 (review)
#53 (review)
#53 (review)
#53 (review)
#53 (review)
#53 (review)