Show follow requsts in notifications w/ accept/reject buttons

Closes #64
This commit is contained in:
Shadowfacts 2020-01-04 23:13:23 -05:00
parent c31916d67e
commit ad09e36907
Signed by: shadowfacts
GPG Key ID: 94A5AB95422746E5
9 changed files with 283 additions and 12 deletions

View File

@ -29,12 +29,12 @@ public class Account: Decodable {
public let fields: [Field]?
public let bot: Bool?
public static func authorizeFollowRequest(_ account: Account) -> Request<Empty> {
return Request<Empty>(method: .post, path: "/api/v1/follow_requests/\(account.id)/authorize")
public static func authorizeFollowRequest(_ account: Account) -> Request<Relationship> {
return Request<Relationship>(method: .post, path: "/api/v1/follow_requests/\(account.id)/authorize")
}
public static func rejectFollowRequest(_ account: Account) -> Request<Empty> {
return Request<Empty>(method: .post, path: "/api/v1/follow_requests/\(account.id)/reject")
public static func rejectFollowRequest(_ account: Account) -> Request<Relationship> {
return Request<Relationship>(method: .post, path: "/api/v1/follow_requests/\(account.id)/reject")
}
public static func removeFromFollowRequests(_ account: Account) -> Request<Empty> {

View File

@ -36,6 +36,7 @@ extension Notification {
case reblog
case favourite
case follow
case followRequest = "follow_request"
}
}

View File

@ -116,6 +116,8 @@
D64BC18623C1253A000D0238 /* AssetPreviewViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64BC18523C1253A000D0238 /* AssetPreviewViewController.swift */; };
D64BC18823C1640A000D0238 /* PinStatusActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64BC18723C1640A000D0238 /* PinStatusActivity.swift */; };
D64BC18A23C16487000D0238 /* UnpinStatusActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64BC18923C16487000D0238 /* UnpinStatusActivity.swift */; };
D64BC18F23C18B9D000D0238 /* FollowRequestNotificationTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64BC18D23C18B9D000D0238 /* FollowRequestNotificationTableViewCell.swift */; };
D64BC19023C18B9D000D0238 /* FollowRequestNotificationTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D64BC18E23C18B9D000D0238 /* FollowRequestNotificationTableViewCell.xib */; };
D64D0AAD2128D88B005A6F37 /* LocalData.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64D0AAC2128D88B005A6F37 /* LocalData.swift */; };
D64D0AB12128D9AE005A6F37 /* OnboardingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64D0AB02128D9AE005A6F37 /* OnboardingViewController.swift */; };
D64F80E2215875CC00BEF393 /* XCBActionType.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64F80E1215875CC00BEF393 /* XCBActionType.swift */; };
@ -384,6 +386,8 @@
D64BC18523C1253A000D0238 /* AssetPreviewViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetPreviewViewController.swift; sourceTree = "<group>"; };
D64BC18723C1640A000D0238 /* PinStatusActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PinStatusActivity.swift; sourceTree = "<group>"; };
D64BC18923C16487000D0238 /* UnpinStatusActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnpinStatusActivity.swift; sourceTree = "<group>"; };
D64BC18D23C18B9D000D0238 /* FollowRequestNotificationTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowRequestNotificationTableViewCell.swift; sourceTree = "<group>"; };
D64BC18E23C18B9D000D0238 /* FollowRequestNotificationTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = FollowRequestNotificationTableViewCell.xib; sourceTree = "<group>"; };
D64D0AAC2128D88B005A6F37 /* LocalData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalData.swift; sourceTree = "<group>"; };
D64D0AB02128D9AE005A6F37 /* OnboardingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingViewController.swift; sourceTree = "<group>"; };
D64F80E1215875CC00BEF393 /* XCBActionType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCBActionType.swift; sourceTree = "<group>"; };
@ -906,6 +910,8 @@
D6A3BC7B232195C600FD64D5 /* ActionNotificationGroupTableViewCell.xib */,
D6A3BC7E2321B7E600FD64D5 /* FollowNotificationGroupTableViewCell.swift */,
D6A3BC7F2321B7E600FD64D5 /* FollowNotificationGroupTableViewCell.xib */,
D64BC18D23C18B9D000D0238 /* FollowRequestNotificationTableViewCell.swift */,
D64BC18E23C18B9D000D0238 /* FollowRequestNotificationTableViewCell.xib */,
);
path = Notifications;
sourceTree = "<group>";
@ -1482,6 +1488,7 @@
D626493323BD751600612E6E /* ShowCameraCollectionViewCell.xib in Resources */,
D626493D23C1000300612E6E /* AlbumTableViewCell.xib in Resources */,
D627944723A6AC9300D38C68 /* BasicTableViewCell.xib in Resources */,
D64BC19023C18B9D000D0238 /* FollowRequestNotificationTableViewCell.xib in Resources */,
D67C57B221E28FAD00C3118B /* ComposeStatusReplyView.xib in Resources */,
0411610122B442870030A9B7 /* AttachmentViewController.xib in Resources */,
D6A3BC812321B7E600FD64D5 /* FollowNotificationGroupTableViewCell.xib in Resources */,
@ -1614,6 +1621,7 @@
D6BC9DB3232D4C07002CA326 /* WellnessPrefsView.swift in Sources */,
D6D58DF922074B74009C8DD9 /* LinkLabel.swift in Sources */,
0454DDAF22B462EF00B8BB8E /* GalleryExpandAnimationController.swift in Sources */,
D64BC18F23C18B9D000D0238 /* FollowRequestNotificationTableViewCell.swift in Sources */,
D6A3BC8A2321F79B00FD64D5 /* AccountTableViewCell.swift in Sources */,
D66A77BB233838DC0058F1EC /* UIFont+Traits.swift in Sources */,
D68FEC4F232C5BC300C84F23 /* SegmentedPageViewController.swift in Sources */,

View File

@ -12,7 +12,9 @@ import Pachyderm
extension Account {
var realDisplayName: String {
if Preferences.shared.hideCustomEmojiInUsernames {
if displayName.isEmpty {
return username
} else if Preferences.shared.hideCustomEmojiInUsernames {
return stripCustomEmoji(from: displayName)
} else {
return displayName

View File

@ -13,7 +13,7 @@ class NotificationsPageViewController: SegmentedPageViewController {
private let notificationsTitle = NSLocalizedString("Notifications", comment: "notifications tab title")
private let mentionsTitle = NSLocalizedString("Mentions", comment: "mentions tab title")
init() {
let notifications = NotificationsTableViewController(allowedTypes: Pachyderm.Notification.Kind.allCases)
notifications.title = notificationsTitle

View File

@ -14,6 +14,7 @@ class NotificationsTableViewController: EnhancedTableViewController {
private let statusCell = "statusCell"
private let actionGroupCell = "actionGroupCell"
private let followGroupCell = "followGroupCell"
private let followRequestCell = "followRequestCell"
let excludedTypes: [Pachyderm.Notification.Kind]
let groupTypes = [Notification.Kind.favourite, .reblog, .follow]
@ -48,9 +49,10 @@ class NotificationsTableViewController: EnhancedTableViewController {
tableView.rowHeight = UITableView.automaticDimension
tableView.estimatedRowHeight = 140
tableView.register(UINib(nibName: "TimelineStatusTableViewCell", bundle: nil), forCellReuseIdentifier: statusCell)
tableView.register(UINib(nibName: "ActionNotificationGroupTableViewCell", bundle: nil), forCellReuseIdentifier: actionGroupCell)
tableView.register(UINib(nibName: "FollowNotificationGroupTableViewCell", bundle: nil), forCellReuseIdentifier: followGroupCell)
tableView.register(UINib(nibName: "TimelineStatusTableViewCell", bundle: .main), forCellReuseIdentifier: statusCell)
tableView.register(UINib(nibName: "ActionNotificationGroupTableViewCell", bundle: .main), forCellReuseIdentifier: actionGroupCell)
tableView.register(UINib(nibName: "FollowNotificationGroupTableViewCell", bundle: .main), forCellReuseIdentifier: followGroupCell)
tableView.register(UINib(nibName: "FollowRequestNotificationTableViewCell", bundle: .main), forCellReuseIdentifier: followRequestCell)
tableView.prefetchDataSource = self
@ -102,10 +104,17 @@ class NotificationsTableViewController: EnhancedTableViewController {
return cell
case .follow:
guard let cell = tableView.dequeueReusableCell(withIdentifier: "followGroupCell", for: indexPath) as? FollowNotificationGroupTableViewCell else { fatalError() }
guard let cell = tableView.dequeueReusableCell(withIdentifier: followGroupCell, for: indexPath) as? FollowNotificationGroupTableViewCell else { fatalError() }
cell.updateUI(group: group)
cell.delegate = self
return cell
case .followRequest:
guard let notification = MastodonCache.notification(for: group.notificationIDs.first!),
let cell = tableView.dequeueReusableCell(withIdentifier: followRequestCell, for: indexPath) as? FollowRequestNotificationTableViewCell else { fatalError() }
cell.updateUI(notification: notification)
cell.delegate = self
return cell
}
}

View File

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="15701" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="15702" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<device id="retina6_1" orientation="portrait" appearance="light"/>
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15703"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15704"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>

View File

@ -0,0 +1,142 @@
//
// FollowRequestNotificationTableViewCell.swift
// Tusker
//
// Created by Shadowfacts on 1/4/20.
// Copyright © 2020 Shadowfacts. All rights reserved.
//
import UIKit
import Pachyderm
class FollowRequestNotificationTableViewCell: UITableViewCell {
var delegate: TuskerNavigationDelegate?
@IBOutlet weak var stackView: UIStackView!
@IBOutlet weak var avatarImageView: UIImageView!
@IBOutlet weak var timestampLabel: UILabel!
@IBOutlet weak var actionLabel: UILabel!
@IBOutlet weak var actionButtonsStackView: UIStackView!
@IBOutlet weak var acceptButton: UIButton!
@IBOutlet weak var rejectButton: UIButton!
var notification: Pachyderm.Notification?
var account: Account!
var updateTimestampWorkItem: DispatchWorkItem?
override func awakeFromNib() {
super.awakeFromNib()
avatarImageView.layer.masksToBounds = true
NotificationCenter.default.addObserver(self, selector: #selector(updateUIForPreferences), name: .preferencesChanged, object: nil)
updateUIForPreferences()
}
@objc func updateUIForPreferences() {
avatarImageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadiusFraction * 30
}
func updateUI(notification: Pachyderm.Notification) {
self.notification = notification
updateUI(account: notification.account)
updateTimestamp()
}
func updateUI(account: Account) {
self.account = account
actionLabel.text = "Request to follow from \(account.realDisplayName)"
ImageCache.avatars.get(account.avatar) { (data) in
guard self.account == account, let data = data, let image = UIImage(data: data) else { return }
DispatchQueue.main.async {
self.avatarImageView.image = image
}
}
}
func updateTimestamp() {
guard let notification = notification else { return }
timestampLabel.text = notification.createdAt.timeAgoString()
let delay: DispatchTimeInterval?
switch notification.createdAt.timeAgo().1 {
case .second:
delay = .seconds(10)
case .minute:
delay = .seconds(60)
default:
delay = nil
}
if let delay = delay {
updateTimestampWorkItem = DispatchWorkItem(block: self.updateTimestamp)
DispatchQueue.main.asyncAfter(deadline: .now() + delay, execute: updateTimestampWorkItem!)
} else {
updateTimestampWorkItem = nil
}
}
override func prepareForReuse() {
super.prepareForReuse()
updateTimestampWorkItem?.cancel()
updateTimestampWorkItem = nil
}
// MARK: - Interaction
@IBAction func rejectButtonPressed() {
let request = Account.rejectFollowRequest(account)
MastodonController.client.run(request) { (response) in
guard case let .success(relationship, _) = response else { fatalError() }
MastodonCache.add(relationship: relationship)
DispatchQueue.main.async {
UINotificationFeedbackGenerator().notificationOccurred(.success)
self.actionButtonsStackView.isHidden = true
let label = UILabel()
label.textAlignment = .center
label.font = .boldSystemFont(ofSize: 17)
label.text = NSLocalizedString("Rejected", comment: "rejected follow request label")
self.stackView.addArrangedSubview(label)
}
}
}
@IBAction func acceptButtonPressed() {
let request = Account.authorizeFollowRequest(account)
MastodonController.client.run(request) { (response) in
guard case let .success(relationship, _) = response else { fatalError() }
MastodonCache.add(relationship: relationship)
DispatchQueue.main.async {
UINotificationFeedbackGenerator().notificationOccurred(.success)
self.actionButtonsStackView.isHidden = true
let label = UILabel()
label.textAlignment = .center
label.font = .boldSystemFont(ofSize: 17)
label.text = NSLocalizedString("Accepted", comment: "accepted follow request label")
self.stackView.addArrangedSubview(label)
}
}
}
}
extension FollowRequestNotificationTableViewCell: SelectableTableViewCell {
func didSelectCell() {
delegate?.selected(account: account.id)
}
}
extension FollowRequestNotificationTableViewCell: MenuPreviewProvider {
var navigationDelegate: TuskerNavigationDelegate? { return delegate }
func getPreviewProviders(for location: CGPoint, sourceViewController: UIViewController) -> PreviewProviders? {
return (content: {
return ProfileTableViewController(accountID: self.account.id)
}, actions: {
return []
})
}
}

View File

@ -0,0 +1,109 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="15702" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<device id="retina6_1" orientation="portrait" appearance="light"/>
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15704"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
<tableViewCell contentMode="scaleToFill" selectionStyle="default" indentationWidth="10" rowHeight="107" id="Pcu-ap-Xqf" customClass="FollowRequestNotificationTableViewCell" customModule="Tusker" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="320" height="107"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="Pcu-ap-Xqf" id="Ulr-P8-MK9">
<rect key="frame" x="0.0" y="0.0" width="320" height="107"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="4" translatesAutoresizingMaskIntoConstraints="NO" id="Cth-1T-Km3">
<rect key="frame" x="74" y="11" width="230" height="85"/>
<subviews>
<stackView opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="G6v-p7-JbC">
<rect key="frame" x="0.0" y="0.0" width="230" height="30"/>
<subviews>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="0j2-g5-Y0W">
<rect key="frame" x="0.0" y="0.0" width="30" height="30"/>
<constraints>
<constraint firstAttribute="width" secondItem="0j2-g5-Y0W" secondAttribute="height" multiplier="1:1" id="05S-TD-ePl"/>
<constraint firstAttribute="height" constant="30" id="KCp-Zt-Cm6"/>
</constraints>
</imageView>
<view contentMode="scaleToFill" horizontalHuggingPriority="249" translatesAutoresizingMaskIntoConstraints="NO" id="9WN-Ql-DDL">
<rect key="frame" x="30" y="0.0" width="175.5" height="30"/>
</view>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" horizontalCompressionResistancePriority="751" text="2m" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Saq-P5-oVH">
<rect key="frame" x="205.5" y="0.0" width="24.5" height="30"/>
<fontDescription key="fontDescription" type="system" weight="light" pointSize="17"/>
<color key="textColor" systemColor="secondaryLabelColor" red="0.23529411759999999" green="0.23529411759999999" blue="0.26274509800000001" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
</label>
</subviews>
</stackView>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Request to follow by Person 1" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="2" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="aM6-C6-9QH">
<rect key="frame" x="0.0" y="34" width="230" height="20.5"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<stackView opaque="NO" contentMode="scaleToFill" distribution="fillEqually" translatesAutoresizingMaskIntoConstraints="NO" id="20F-2n-eQx">
<rect key="frame" x="0.0" y="58.5" width="230" height="26.5"/>
<subviews>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="CMQ-TI-X9k">
<rect key="frame" x="0.0" y="0.0" width="115" height="26.5"/>
<state key="normal" image="checkmark.circle.fill" catalog="system">
<preferredSymbolConfiguration key="preferredSymbolConfiguration" configurationType="pointSize" pointSize="22"/>
</state>
<connections>
<action selector="acceptButtonPressed" destination="Pcu-ap-Xqf" eventType="touchUpInside" id="hGw-3d-RNi"/>
</connections>
</button>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="7MW-rY-m5l">
<rect key="frame" x="115" y="0.0" width="115" height="26.5"/>
<state key="normal" image="xmark.circle.fill" catalog="system">
<preferredSymbolConfiguration key="preferredSymbolConfiguration" configurationType="pointSize" pointSize="22"/>
</state>
<connections>
<action selector="rejectButtonPressed" destination="Pcu-ap-Xqf" eventType="touchUpInside" id="EP6-Bg-3nC"/>
</connections>
</button>
</subviews>
</stackView>
</subviews>
</stackView>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="person.fill" catalog="system" translatesAutoresizingMaskIntoConstraints="NO" id="1qX-LD-7ZK">
<rect key="frame" x="36" y="12.5" width="30" height="27"/>
<constraints>
<constraint firstAttribute="height" constant="30" id="UDV-Vb-1bn"/>
<constraint firstAttribute="width" constant="30" id="d5A-cf-hFe"/>
</constraints>
</imageView>
</subviews>
<constraints>
<constraint firstAttribute="trailingMargin" secondItem="Cth-1T-Km3" secondAttribute="trailing" id="9Zn-8U-6HF"/>
<constraint firstItem="Cth-1T-Km3" firstAttribute="top" secondItem="Ulr-P8-MK9" secondAttribute="topMargin" id="EIi-sE-AkN"/>
<constraint firstItem="1qX-LD-7ZK" firstAttribute="top" secondItem="Ulr-P8-MK9" secondAttribute="topMargin" id="GUW-Xm-fLN"/>
<constraint firstItem="Cth-1T-Km3" firstAttribute="leading" secondItem="Ulr-P8-MK9" secondAttribute="leadingMargin" constant="58" id="QvY-68-add"/>
<constraint firstAttribute="bottomMargin" secondItem="Cth-1T-Km3" secondAttribute="bottom" id="aje-GB-qn6"/>
<constraint firstItem="Cth-1T-Km3" firstAttribute="leading" secondItem="1qX-LD-7ZK" secondAttribute="trailing" constant="8" id="qnO-DF-3wu"/>
</constraints>
</tableViewCellContentView>
<viewLayoutGuide key="safeArea" id="ctM-Hq-1Oz"/>
<connections>
<outlet property="acceptButton" destination="CMQ-TI-X9k" id="xL1-MG-SHi"/>
<outlet property="actionButtonsStackView" destination="20F-2n-eQx" id="Uaj-3F-N05"/>
<outlet property="actionLabel" destination="aM6-C6-9QH" id="UfY-EF-7Ya"/>
<outlet property="avatarImageView" destination="0j2-g5-Y0W" id="3Qj-5q-e73"/>
<outlet property="rejectButton" destination="7MW-rY-m5l" id="ZeH-FG-T7M"/>
<outlet property="stackView" destination="Cth-1T-Km3" id="Elz-8v-AFa"/>
<outlet property="timestampLabel" destination="Saq-P5-oVH" id="d6F-HV-HXs"/>
</connections>
<point key="canvasLocation" x="131.8840579710145" y="174.44196428571428"/>
</tableViewCell>
</objects>
<resources>
<image name="checkmark.circle.fill" catalog="system" width="64" height="60"/>
<image name="person.fill" catalog="system" width="64" height="60"/>
<image name="xmark.circle.fill" catalog="system" width="64" height="60"/>
</resources>
</document>