diff --git a/apps/cory/ssb.json b/apps/cory/ssb.json new file mode 100644 index 00000000..6b7f9f49 --- /dev/null +++ b/apps/cory/ssb.json @@ -0,0 +1 @@ +{"type":"tildefriends-app","files":{"app.js":"&nbY2UrHp+GcW1FrromCZRyJNg7in7InypXSaFNipTrs=.sha256","index.html":"&xCI4SATYvlJkVX5EdlRROoDSMWlajF+wDFrWSUYZqd8=.sha256","vue-material.js":"&K5cdLqXYCENPak/TCINHQhyJhpS4G9DlZHGwoh/LF2g=.sha256","tf-user.js":"&DdJwZYEo7AqFyutYMvEjykoVXxdHVog0UXye6Sbo0TU=.sha256","tf-message.js":"&kIpc5B2dt4oefsTgNASz2cVte3WRO0k2NCYJYRzu/MA=.sha256","tf.js":"&WvteLAg4G92YOUO3/B36kmar5lqFq8Pil4rsy7uFNDY=.sha256","commonmark.min.js":"&EP0OeR9zyLwZannz+0ga4s9AGES2RLvvIIQYHqqV6+k=.sha256","vue.js":"&g1wvA+yHl1sVC+eufTsg9If7ZeVyMTBU+h0tks7ZNzE=.sha256","vue-material-theme-default-dark.css":"&RP2nr+2CR18BpHHw5ST9a5GJUCOG9n0G2kuGkcQioWE=.sha256","vue-material.min.css":"&kGbUM2QgFSyHZRzqQb0b+0S3EVIlZ0AXpdiAVjIhou8=.sha256","roboto.css":"&jJv43Om673mQO5JK0jj7714s5E+5Yrf82H6LcDx7wUs=.sha256","material-icons.css":"&a28PdcVvgq/DxyIvJAx/e+ZOEtOuHnr3kjLWKyzH11M=.sha256","tf-shared.js":"&g17mCj0bfk0tw0sYk2VMU67Bt8Hdznjl44E01TxQF5Y=.sha256"}} \ No newline at end of file diff --git a/apps/cory/ssb/app.js b/apps/cory/ssb/app.js new file mode 100644 index 00000000..3162e5d4 --- /dev/null +++ b/apps/cory/ssb/app.js @@ -0,0 +1,562 @@ +"use strict"; + +const k_posts_max = 20; +const k_votes_max = 20; + +var g_ready = false; +var g_selected = null; + +var g_blocking_cache = {}; +var g_following_cache = {}; +var g_following_deep_cache = {}; +var g_sequence = {}; + +async function following(db, id) { + if (g_following_cache[id]) { + return g_following_cache[id]; + } + var o = await db.get(id + ":following"); + const k_version = 5; + var f = o ? JSON.parse(o) : o; + if (!f || f.version != k_version) { + f = {users: [], sequence: 0, version: k_version}; + } + f.users = new Set(f.users); + await ssb.sqlStream( + "SELECT "+ + " sequence, "+ + " json_extract(content, '$.contact') AS contact, "+ + " json_extract(content, '$.following') AS following "+ + "FROM messages "+ + "WHERE "+ + " author = ?1 AND "+ + " sequence > ?2 AND "+ + " json_extract(content, '$.type') = 'contact' "+ + "UNION SELECT MAX(sequence) AS sequence, NULL, NULL FROM messages WHERE author = ?1 "+ + "ORDER BY sequence", + [id, f.sequence], + function(row) { + if (row.following) { + f.users.add(row.contact); + } else { + f.users.delete(row.contact); + } + if (row.sequence) { + f.sequence = row.sequence; + } + }); + g_sequence[id] = f.sequence; + var as_set = f.users; + f.users = Array.from(f.users).sort(); + var j = JSON.stringify(f); + if (o != j) { + await db.set(id + ":following", j); + } + f.users = as_set; + g_following_cache[id] = f.users; + return f.users; +} + +async function followingDeep(db, seed_ids, depth, blocked) { + if (depth <= 0) { + return seed_ids; + } + var key = JSON.stringify([seed_ids, depth, blocked]); + if (g_following_deep_cache[key]) { + return g_following_deep_cache[key]; + } + var f = await Promise.all(seed_ids.map(x => following(db, x).then(x => [...x]))); + var ids = [].concat(...f); + if (blocked) { + ids = ids.filter(x => !blocked.has(x)); + } + var x = await followingDeep(db, [...new Set(ids)].sort(), depth - 1, blocked); + x = [...new Set([].concat(...x, ...seed_ids))].sort(); + g_following_deep_cache[key] = x; + return x; +} + +async function blocking(db, id) { + if (g_blocking_cache[id]) { + return g_blocking_cache[id]; + } + var o = await db.get(id + ":blocking"); + const k_version = 5; + var f = o ? JSON.parse(o) : o; + if (!f || f.version != k_version) { + f = {users: [], sequence: 0, version: k_version}; + } + f.users = new Set(f.users); + if (!g_sequence[id] || g_sequence[id] > f.sequence) { + await ssb.sqlStream( + "SELECT "+ + " sequence, "+ + " json_extract(content, '$.contact') AS contact, "+ + " json_extract(content, '$.blocking') AS blocking "+ + "FROM messages "+ + "WHERE "+ + " author = ?1 AND "+ + " sequence > ?2 AND "+ + " json_extract(content, '$.type') = 'contact' "+ + "UNION SELECT MAX(sequence) AS sequence, NULL, NULL FROM messages WHERE author = ?1 "+ + "ORDER BY sequence", + [id, f.sequence], + function(row) { + if (row.blocking) { + f.users.add(row.contact); + } else { + f.users.delete(row.contact); + } + if (row.sequence) { + f.sequence = row.sequence; + } + }); + g_sequence[id] = f.sequence; + } + var as_set = f.users; + f.users = Array.from(f.users).sort(); + var j = JSON.stringify(f); + if (o != j) { + await db.set(id + ":blocking", j); + } + f.users = as_set; + g_blocking_cache[id] = f.users; + return f.users; +} + +async function getAbout(db, id) { + var o = await db.get(id + ":about"); + const k_version = 5; + var f = o ? JSON.parse(o) : o; + if (!f || f.version != k_version) { + f = {about: {}, sequence: 0, version: k_version}; + } + if (g_sequence[id] === undefined || g_sequence[id] > f.sequence) { + await ssb.sqlStream( + "SELECT "+ + " sequence, "+ + " content "+ + "FROM messages "+ + "WHERE "+ + " author = ?1 AND "+ + " sequence > ?2 AND "+ + " json_extract(content, '$.type') = 'about' AND "+ + " json_extract(content, '$.about') = ?1 "+ + "UNION SELECT MAX(sequence) as sequence, NULL FROM messages WHERE author = ?1 "+ + "ORDER BY sequence", + [id, f.sequence], + function(row) { + if (row.content) { + var about = {}; + try { + about = JSON.parse(row.content); + } catch { + } + delete about.about; + delete about.type; + f.about = Object.assign(f.about, about); + } + if (row.sequence) { + f.sequence = Math.max(f.sequence, row.sequence); + } + }); + g_sequence[id] = f.sequence; + var j = JSON.stringify(f); + if (o != j) { + await db.set(id + ":about", j); + } + } + return f.about; +} + +function fnv32a(value) +{ + var result = 0x811c9dc5; + for (var i = 0; i < value.length; i++) { + result ^= value.charCodeAt(i); + result += (result << 1) + (result << 4) + (result << 7) + (result << 8) + (result << 24); + } + return result >>> 0; +} + +async function getRecentPostsSingleId(db, id, limit) { + var recent = []; + await ssb.sqlStream( + "SELECT "+ + " rowid, "+ + " id, "+ + " timestamp "+ + "FROM messages "+ + "WHERE "+ + " author = ? AND "+ + " json_extract(content, '$.type') = 'post' "+ + "ORDER BY sequence DESC LIMIT ?", + [id, limit], + function(row) { + if (row.id) { + recent.push({id: row.id, timestamp: row.timestamp}); + } + }); + recent.sort((x, y) => y.timestamp - x.timestamp); + return recent.map(x => x.id); +} + +async function getRecentPostIds(db, id, ids, limit) { + if (ids.length == 1) { + return await getRecentPostsSingleId(db, ids[0], limit); + } + const k_version = 11; + const k_batch_max = 32; + var o = await db.get(id + ':recent_posts'); + var recent = []; + var f = o ? JSON.parse(o) : o; + var ids_hash = fnv32a(JSON.stringify(ids)); + if (!f || f.version != k_version || f.ids_hash != ids_hash) { + f = {recent: [], rowid: 0, version: k_version, ids_hash: ids_hash}; + } + var row_id_max = 0; + await ssb.sqlStream( + "SELECT MAX(rowid) as rowid FROM messages", + [], + function(row) { + row_id_max = row.rowid; + }); + for (var i = 0; i < ids.length; i += k_batch_max) { + var ids_batch = ids.slice(i, Math.min(i + k_batch_max, ids.length)); + await ssb.sqlStream( + "SELECT "+ + " rowid, "+ + " id, "+ + " timestamp "+ + "FROM messages "+ + "WHERE "+ + " rowid > ? AND "+ + " rowid <= ? AND "+ + " author IN (" + ids_batch.map(x => '?').join(", ") + ") "+ + "ORDER BY timestamp DESC LIMIT ?", + [].concat([f.rowid, row_id_max], ids_batch, [limit]), + function(row) { + if (row.id) { + recent.push({id: row.id, timestamp: row.timestamp}); + } + }); + } + f.rowid = row_id_max; + f.recent = [].concat(recent, f.recent); + var have = {}; + f.recent = f.recent.filter(function(x) { + if (!have[x.id]) { + have[x.id] = true; + return true; + } + }); + f.recent.sort((x, y) => y.timestamp - x.timestamp); + f.recent = f.recent.slice(0, limit); + var j = JSON.stringify(f); + if (o != j) { + await db.set(id + ":recent_posts", j); + } + return f.recent.map(x => x.id); +} + +async function getRelatedPostIds(db, message, ids, limit) { + const k_batch_max = 16; + var recent = []; + var row_id_max = 0; + await ssb.sqlStream( + "SELECT MAX(rowid) as rowid FROM messages", + [], + function(row) { + row_id_max = row.rowid; + }); + var id = message.id; + try { + id = JSON.parse(message.content).root || id; + } catch { + } + for (var i = 0; i < ids.length; i += k_batch_max) { + var ids_batch = ids.slice(i, Math.min(i + k_batch_max, ids.length)); + await ssb.sqlStream( + "SELECT "+ + " rowid, "+ + " id, "+ + " timestamp "+ + "FROM messages "+ + "WHERE "+ + " timestamp >= ? AND "+ + " rowid <= ? AND "+ + " author IN (" + ids_batch.map(x => '?').join(", ") + ") AND "+ + " json_extract(content, '$.type') = 'post' AND "+ + " (id = ? OR json_extract(content, '$.root') = ?) "+ + "ORDER BY timestamp DESC LIMIT ?", + [].concat([message.timestamp || 0, row_id_max], ids_batch, [message.id, id, limit]), + function(row) { + if (row.id) { + recent.push({id: row.id, timestamp: row.timestamp}); + } + }); + } + recent.sort((x, y) => y.timestamp - x.timestamp); + recent = recent.slice(0, limit); + return recent.map(x => x.id); +} + +async function getVotes(db, id) { + var o = await db.get(id + ":votes"); + const k_version = 7; + var f = o ? JSON.parse(o) : o; + if (!f || f.version != k_version) { + f = {votes: [], sequence: 0, version: k_version}; + } + if (g_sequence[id] === undefined || g_sequence[id] > f.sequence) { + var votes = []; + await ssb.sqlStream( + "SELECT "+ + " author, "+ + " id, "+ + " sequence, "+ + " timestamp, "+ + " content "+ + "FROM messages "+ + "WHERE "+ + " author = ? AND "+ + " sequence > ? AND "+ + " json_extract(content, '$.type') = 'vote' "+ + "UNION SELECT NULL, NULL, MAX(sequence) AS sequence, NULL, NULL FROM messages WHERE author = ? "+ + "ORDER BY sequence DESC LIMIT ?", + [id, f.sequence, id, k_votes_max], + function(row) { + if (row.id) { + votes.push(row); + } + if (row.sequence) { + f.sequence = Math.max(f.sequence, row.sequence); + } + }); + g_sequence[id] = f.sequence; + f.votes = [].concat(votes, f.votes).slice(0, k_votes_max); + var j = JSON.stringify(f); + if (o != j) { + await db.set(id + ":votes", j); + } + } + return f.votes; +} + +async function getPosts(db, ids) { + var posts = []; + if (ids.length) { + await ssb.sqlStream( + "SELECT rowid, * FROM messages WHERE id IN (" + ids.map(x => "?").join(", ") + ") ORDER BY timestamp DESC", + ids, + row => posts.push(row)); + } + return posts; +} + +async function ready() { + g_ready = true; + return refresh(g_selected); +} + +ssb.addEventListener('broadcasts', async function() { + await app.postMessage({broadcasts: await ssb.getBroadcasts()}); +}); + +core.register('onConnectionsChanged', async function() { + var connections = await ssb.connections(); + await app.postMessage({connections: connections}); +}); + +async function updateSequences(db) { + var k_batch_max = 100; + var changes = {}; + var keys = Object.keys(g_sequence); + for (var i = 0; i < keys.length; i += k_batch_max) { + var ids_batch = keys.slice(i, Math.min(i + k_batch_max, keys.length)); + await ssb.sqlStream( + "SELECT "+ + " author, "+ + " MAX(sequence) AS sequence "+ + "FROM messages "+ + "WHERE "+ + " author IN (" + ids_batch.map(x => '?').join(", ") + ")", + ids_batch, + function(row) { + if (g_sequence[row.author] != row.sequence) { + g_sequence[row.author] = row.sequence; + changes[row.author] = row.sequence; + } + }); + } + return changes; +} + +async function refresh(selected) { + var timing = []; + timing.push({name: 'start', time: new Date()}); + g_following_cache = {}; + g_following_deep_cache = {}; + await app.postMessage({clear: true}); + var whoami = await ssb.whoami(); + var db = await database("ssb"); + g_sequence = {}; + try { + g_sequence = JSON.parse(await db.get('sequence')); + } catch (e) { + } + await updateSequences(db); + timing.push({name: 'init', time: new Date()}); + var blocked = await blocking(db, whoami); + timing.push({name: 'blocked', time: new Date()}); + var all_followed = await followingDeep(db, [whoami], 2, blocked); + timing.push({name: 'all_followed', time: new Date()}); + if (selected) { + g_selected = selected; + } else { + g_selected = all_followed; + } + await Promise.all([ + app.postMessage({whoami: whoami}), + app.postMessage({hash: selected && selected.length == 1 ? selected[0] : null}), + ssb.getBroadcasts().then(broadcasts => app.postMessage({broadcasts: broadcasts})), + ssb.connections().then(connections => app.postMessage({connections: connections})), + core.apps().then(apps => app.postMessage({apps: apps})), + ]); + timing.push({name: 'core', time: new Date()}); + var ids; + if (selected && selected.length == 1 && selected[0].startsWith('%')) { + var m = await getPosts(db, selected); + m = m.length ? m[0] : {id: selected[0]}; + ids = await getRelatedPostIds(db, m, all_followed, k_posts_max); + } else { + ids = await getRecentPostIds(db, whoami, g_selected, k_posts_max); + } + timing.push({name: 'get_post_ids', time: new Date()}); + var posts = await getPosts(db, ids); + timing.push({name: 'get_posts', time: new Date()}); + var roots = posts.map(function(x) { + try { + return JSON.parse(x.content).root; + } catch { + return null; + } + }); + var have = new Set(posts.map(x => x.id)); + roots = [...new Set(roots)].filter(x => x && !have.has(x)); + var all_posts = [].concat(posts, await getPosts(db, roots)); + timing.push({name: 'get_root_posts', time: new Date()}); + await Promise.all(all_posts.map(x => app.postMessage({message: x}))); + timing.push({name: 'send_posts', time: new Date()}); + await Promise.all(all_followed.map(id => getAbout(db, id).then(results => Object.keys(results).length ? app.postMessage({user: {user: id, about: results}}) : null))); + timing.push({name: 'about', time: new Date()}); + var all_votes = []; + for (let id of all_followed) { + var results = await getVotes(db, id); + if (results.length) { + all_votes.push(results); + } + } + all_votes = all_votes.flat(); + const k_votes_send_max = 2048; + for (var i = 0; i < all_votes.length; i += k_votes_send_max) { + var votes = all_votes.slice(i, Math.min(i + k_votes_send_max, all_votes.length)); + await app.postMessage({votes: votes}); + } + timing.push({name: 'votes', time: new Date()}); + await all_followed.map( + id => app.postMessage( + { + following: { + id: id, + users: [...(g_following_cache[id] || [])], + } + } + ) + ); + timing.push({name: 'following', time: new Date()}); + await app.postMessage({blocking: {id: whoami, users: [...(g_blocking_cache[whoami] || [])]}}); + timing.push({name: 'send_blocking', time: new Date()}); + await db.set('sequence', JSON.stringify(g_sequence)); + + var times = {}; + var previous = null; + for (let t of timing) { + times[t.name] = (t.time - (previous || t).time) / 1000.0 + ' s'; + previous = t; + } + await app.postMessage({ready: true, times: times}); +} + +ssb.addEventListener('message', async function(id) { + var db = await database("ssb"); + var posts = await getPosts(db, [id]); + for (let post of posts) { + if (post.author == await ssb.whoami() || + JSON.parse(post.content).type != 'post') { + await app.postMessage({message: post}); + } else { + await app.postMessage({unread: 1}); + } + } +}); + +async function addAppSources(message) { + if (message.mentions) { + for (let mention of message.mentions) { + if (mention.type == 'application/tildefriends') { + var blob = await ssb.blobGet(mention.link); + var json = JSON.parse(utf8Decode(blob)); + for (let file of Object.keys(json.files)) { + message.mentions.push({ + name: file, + link: json.files[file], + }); + } + } + } + } +} + +core.register('message', async function(m) { + if (m.message == 'ready') { + await ready(); + } else if (m.message) { + if (m.message.connect) { + await ssb.connect(m.message.connect); + } else if (m.message.appendMessage) { + await addAppSources(m.message.appendMessage); + await ssb.appendMessage(m.message.appendMessage); + } else if (m.message.refresh) { + await refresh(g_selected); + } + } else if (m.event == 'hashChange') { + if (m.hash.length > 1) { + g_selected = [m.hash.substring(1)]; + } else { + g_selected = null; + } + if (g_ready) { + await refresh(g_selected); + } + } else if (m.event == 'storeBlobComplete') { + await app.postMessage({storeBlobComplete: m.path}); + } else if (m.event == 'focus' || m.event == 'blur') { + /* Shh. */ + } else { + print(JSON.stringify(m)); + } +}); + +async function main() { + if (core.user && + core.user.credentials && + core.user.credentials.permissions && + core.user.credentials.permissions.administration) { + await app.setDocument(utf8Decode(await getFile("index.html"))); + } else { + await app.setDocument('
{{content_raw}}+
, or missing
. Bailing hydration and performing ' + + 'full client-side render.' + ); + } + } + // either not server-rendered, or hydration failed. + // create an empty node and replace it + oldVnode = emptyNodeAt(oldVnode); + } + + // replacing existing element + var oldElm = oldVnode.elm; + var parentElm = nodeOps.parentNode(oldElm); + + // create new node + createElm( + vnode, + insertedVnodeQueue, + // extremely rare edge case: do not insert if old element is in a + // leaving transition. Only happens when combining transition + + // keep-alive + HOCs. (#4590) + oldElm._leaveCb ? null : parentElm, + nodeOps.nextSibling(oldElm) + ); + + // update parent placeholder node element, recursively + if (isDef(vnode.parent)) { + var ancestor = vnode.parent; + var patchable = isPatchable(vnode); + while (ancestor) { + for (var i = 0; i < cbs.destroy.length; ++i) { + cbs.destroy[i](ancestor); + } + ancestor.elm = vnode.elm; + if (patchable) { + for (var i$1 = 0; i$1 < cbs.create.length; ++i$1) { + cbs.create[i$1](emptyNode, ancestor); + } + // #6513 + // invoke insert hooks that may have been merged by create hooks. + // e.g. for directives that uses the "inserted" hook. + var insert = ancestor.data.hook.insert; + if (insert.merged) { + // start at index 1 to avoid re-invoking component mounted hook + for (var i$2 = 1; i$2 < insert.fns.length; i$2++) { + insert.fns[i$2](); + } + } + } else { + registerRef(ancestor); + } + ancestor = ancestor.parent; + } + } + + // destroy old node + if (isDef(parentElm)) { + removeVnodes([oldVnode], 0, 0); + } else if (isDef(oldVnode.tag)) { + invokeDestroyHook(oldVnode); + } + } + } + + invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch); + return vnode.elm + } + } + + /* */ + + var directives = { + create: updateDirectives, + update: updateDirectives, + destroy: function unbindDirectives (vnode) { + updateDirectives(vnode, emptyNode); + } + }; + + function updateDirectives (oldVnode, vnode) { + if (oldVnode.data.directives || vnode.data.directives) { + _update(oldVnode, vnode); + } + } + + function _update (oldVnode, vnode) { + var isCreate = oldVnode === emptyNode; + var isDestroy = vnode === emptyNode; + var oldDirs = normalizeDirectives$1(oldVnode.data.directives, oldVnode.context); + var newDirs = normalizeDirectives$1(vnode.data.directives, vnode.context); + + var dirsWithInsert = []; + var dirsWithPostpatch = []; + + var key, oldDir, dir; + for (key in newDirs) { + oldDir = oldDirs[key]; + dir = newDirs[key]; + if (!oldDir) { + // new directive, bind + callHook$1(dir, 'bind', vnode, oldVnode); + if (dir.def && dir.def.inserted) { + dirsWithInsert.push(dir); + } + } else { + // existing directive, update + dir.oldValue = oldDir.value; + dir.oldArg = oldDir.arg; + callHook$1(dir, 'update', vnode, oldVnode); + if (dir.def && dir.def.componentUpdated) { + dirsWithPostpatch.push(dir); + } + } + } + + if (dirsWithInsert.length) { + var callInsert = function () { + for (var i = 0; i < dirsWithInsert.length; i++) { + callHook$1(dirsWithInsert[i], 'inserted', vnode, oldVnode); + } + }; + if (isCreate) { + mergeVNodeHook(vnode, 'insert', callInsert); + } else { + callInsert(); + } + } + + if (dirsWithPostpatch.length) { + mergeVNodeHook(vnode, 'postpatch', function () { + for (var i = 0; i < dirsWithPostpatch.length; i++) { + callHook$1(dirsWithPostpatch[i], 'componentUpdated', vnode, oldVnode); + } + }); + } + + if (!isCreate) { + for (key in oldDirs) { + if (!newDirs[key]) { + // no longer present, unbind + callHook$1(oldDirs[key], 'unbind', oldVnode, oldVnode, isDestroy); + } + } + } + } + + var emptyModifiers = Object.create(null); + + function normalizeDirectives$1 ( + dirs, + vm + ) { + var res = Object.create(null); + if (!dirs) { + // $flow-disable-line + return res + } + var i, dir; + for (i = 0; i < dirs.length; i++) { + dir = dirs[i]; + if (!dir.modifiers) { + // $flow-disable-line + dir.modifiers = emptyModifiers; + } + res[getRawDirName(dir)] = dir; + dir.def = resolveAsset(vm.$options, 'directives', dir.name, true); + } + // $flow-disable-line + return res + } + + function getRawDirName (dir) { + return dir.rawName || ((dir.name) + "." + (Object.keys(dir.modifiers || {}).join('.'))) + } + + function callHook$1 (dir, hook, vnode, oldVnode, isDestroy) { + var fn = dir.def && dir.def[hook]; + if (fn) { + try { + fn(vnode.elm, dir, vnode, oldVnode, isDestroy); + } catch (e) { + handleError(e, vnode.context, ("directive " + (dir.name) + " " + hook + " hook")); + } + } + } + + var baseModules = [ + ref, + directives + ]; + + /* */ + + function updateAttrs (oldVnode, vnode) { + var opts = vnode.componentOptions; + if (isDef(opts) && opts.Ctor.options.inheritAttrs === false) { + return + } + if (isUndef(oldVnode.data.attrs) && isUndef(vnode.data.attrs)) { + return + } + var key, cur, old; + var elm = vnode.elm; + var oldAttrs = oldVnode.data.attrs || {}; + var attrs = vnode.data.attrs || {}; + // clone observed objects, as the user probably wants to mutate it + if (isDef(attrs.__ob__)) { + attrs = vnode.data.attrs = extend({}, attrs); + } + + for (key in attrs) { + cur = attrs[key]; + old = oldAttrs[key]; + if (old !== cur) { + setAttr(elm, key, cur, vnode.data.pre); + } + } + // #4391: in IE9, setting type can reset value for input[type=radio] + // #6666: IE/Edge forces progress value down to 1 before setting a max + /* istanbul ignore if */ + if ((isIE || isEdge) && attrs.value !== oldAttrs.value) { + setAttr(elm, 'value', attrs.value); + } + for (key in oldAttrs) { + if (isUndef(attrs[key])) { + if (isXlink(key)) { + elm.removeAttributeNS(xlinkNS, getXlinkProp(key)); + } else if (!isEnumeratedAttr(key)) { + elm.removeAttribute(key); + } + } + } + } + + function setAttr (el, key, value, isInPre) { + if (isInPre || el.tagName.indexOf('-') > -1) { + baseSetAttr(el, key, value); + } else if (isBooleanAttr(key)) { + // set attribute for blank value + // e.g. + if (isFalsyAttrValue(value)) { + el.removeAttribute(key); + } else { + // technically allowfullscreen is a boolean attribute for