diff options
author | Matias Linares <matias@deprecated.org> | 2024-10-21 09:57:10 -0300 |
---|---|---|
committer | Matias Linares <matias.linares@comprandoengrupo.net> | 2024-10-29 18:11:18 -0300 |
commit | a88655cd202dc43a64a7107f6f533bd386079386 (patch) | |
tree | ae6038d67657aa5bf1c18f2e2d1e6dc0bb1558ac /kodereviewer | |
parent | d349e5014fc60a8ae140a56c457c3f3258959582 (diff) | |
download | kodereviewer-a88655cd202dc43a64a7107f6f533bd386079386.tar.gz |
Add reviews view :D
Diffstat (limited to 'kodereviewer')
-rw-r--r-- | kodereviewer/app.py | 3 | ||||
-rw-r--r-- | kodereviewer/data.py | 96 | ||||
-rw-r--r-- | kodereviewer/models/__init__.py | 2 | ||||
-rw-r--r-- | kodereviewer/models/comments.py | 12 | ||||
-rw-r--r-- | kodereviewer/models/file.py | 2 | ||||
-rw-r--r-- | kodereviewer/models/reviews.py | 295 | ||||
-rw-r--r-- | kodereviewer/network_manager.py | 14 | ||||
-rw-r--r-- | kodereviewer/qml/Editor.qml | 2 | ||||
-rw-r--r-- | kodereviewer/qml/Main.qml | 2 | ||||
-rw-r--r-- | kodereviewer/qml/MarkdownTextArea.qml | 3 | ||||
-rw-r--r-- | kodereviewer/qml/PullRequestPage.qml | 56 | ||||
-rw-r--r-- | kodereviewer/qml/ReviewList.qml | 84 |
12 files changed, 535 insertions, 36 deletions
diff --git a/kodereviewer/app.py b/kodereviewer/app.py index bc5a626..1eeb241 100644 --- a/kodereviewer/app.py +++ b/kodereviewer/app.py @@ -14,7 +14,7 @@ from kodereviewer.models.file import FileModel from kodereviewer.network_manager import NetworkManager from kodereviewer.models import ( CommentModel, TreeFileModel, LabelModel, LineModel, PullRequestModel, - ProjectModel, ReviewerModel + ProjectModel, ReviewerModel, ReviewModel ) import logging from rich.logging import RichHandler @@ -54,6 +54,7 @@ def main(): qmlRegisterType(LineModel, "org.deprecated.kodereviewer", 1, 0, "LineModel") qmlRegisterType(PullRequestModel, "org.deprecated.kodereviewer", 1, 0, "PullRequestModel") qmlRegisterType(ReviewerModel, "org.deprecated.kodereviewer", 1, 0, "ReviewerModel") + qmlRegisterType(ReviewModel, "org.deprecated.kodereviewer", 1, 0, "ReviewModel") qmlRegisterType(NetworkManager, "org.deprecated.kodereviewer", 1, 0, "NetworkManager") localized_context = KLocalizedContext() diff --git a/kodereviewer/data.py b/kodereviewer/data.py index 61f1a01..1598e9d 100644 --- a/kodereviewer/data.py +++ b/kodereviewer/data.py @@ -10,13 +10,22 @@ from PySide6.QtQml import QmlElement class User(QObject): - username: str - avatar_url: str + + _username: str + _avatar_url: str def __init__(self, data: dict[str, Any]): super().__init__() - self.username = data['login'] - self.avatar_url = data['avatar_url'] + self._username = data['login'] + self._avatar_url = data['avatar_url'] + + @Property(str, constant=True) + def username(self) -> str: + return self._username + + @Property(str, constant=True) + def avatarUrl(self) -> str: + return self._avatar_url class Label(QObject): @@ -32,21 +41,68 @@ class Label(QObject): class Comment(QObject): - body: str - reactions: dict[str, int] + _body: str + _reactions: dict[str, int] - created_at: datetime - updated_at: datetime + _created_at: datetime + _updated_at: datetime - user: User + _user: User def __init__(self, data: dict[str, Any]): super().__init__() - self.body = data['body'] - self.reactions = data['reactions'] - self.created_at = datetime.fromisoformat(data['created_at']) - self.updated_at = datetime.fromisoformat(data['updated_at']) - self.user = User(data['user']) + self._body = data['body'] + self._reactions = data['reactions'] + self._created_at = datetime.fromisoformat(data['created_at']) + self._updated_at = datetime.fromisoformat(data['updated_at']) + self._user = User(data['user']) + + @Property(str, constant=True) + def body(self) -> str: + return self._body + + @Property(datetime, constant=True) + def created_at(self) -> datetime: + return self._created_at + + @Property(datetime, constant=True) + def updated_at(self) -> datetime: + return self._updated_at + + @Property(User, constant=True) + def user(self) -> User: + return self._user + + +class ReviewComment(Comment): + _id: int + _in_reply_to: int | None + _diff: str + _line: int + def __init__(self, data: dict[str, Any]): + super().__init__(data) + self._id = data['id'] + self._in_reply_to = data.get('in_reply_to_id') + self._diff = data['diff_hunk'] + self._line = data['line'] + + @Property(int, constant=True) + def id(self) -> int: + return self._id + + @Property(int, constant=True) + def in_reply_to(self) -> int: + if self._in_reply_to is None: + return 0 + return self._in_reply_to + + @Property(str, constant=True) + def diff(self) -> str: + return self._diff + + @Property(int, constant=True) + def line(self) -> int: + return self._line class ChangedFileStatus(Enum): @@ -112,12 +168,14 @@ class PullRequest(QObject): _last_commit: str _comments: list[Comment] + _reviews: list[ReviewComment] _files: list[ChangedFile] _initial_data: dict[str, Any] commentsLoaded = Signal() filesLoaded = Signal() + reviewsLoaded = Signal() def __init__(self, data: dict[str, Any], *args, **kwargs): super().__init__(*args, **kwargs) @@ -146,6 +204,7 @@ class PullRequest(QObject): # At first this is empty until it's updated with a request self._comments = [] self._files = [] + self._reviews = [] self._initial_data = data @@ -207,6 +266,10 @@ class PullRequest(QObject): def files(self) -> list[ChangedFile]: return self._files + @Property(list, constant=True) + def reviews(self) -> list[ReviewComment]: + return self._reviews + def load_comments(self, response: QByteArray) -> None: data = json.loads(response.toStdString()) self._comments = [Comment(comment) for comment in data] @@ -217,6 +280,11 @@ class PullRequest(QObject): self._files = [ChangedFile(file) for file in data] self.filesLoaded.emit() + def load_reviews(self, response: QByteArray) -> None: + data = json.loads(response.toStdString()) + self._reviews = [ReviewComment(review) for review in data] + self.reviewsLoaded.emit() + def copy(self): """Create a copy of a PullRequest with it's initial parameters.""" return PullRequest(deepcopy(self._initial_data)) diff --git a/kodereviewer/models/__init__.py b/kodereviewer/models/__init__.py index 810d403..aa7bb7d 100644 --- a/kodereviewer/models/__init__.py +++ b/kodereviewer/models/__init__.py @@ -5,6 +5,7 @@ from kodereviewer.models.project import ProjectModel from kodereviewer.models.pull_request import PullRequestModel from kodereviewer.models.label import LabelModel from kodereviewer.models.reviewer import ReviewerModel +from kodereviewer.models.reviews import ReviewModel __all__ = [ 'CommentModel', @@ -15,4 +16,5 @@ __all__ = [ 'ProjectModel', 'PullRequestModel', 'ReviewerModel', + 'ReviewModel' ] diff --git a/kodereviewer/models/comments.py b/kodereviewer/models/comments.py index 0d92e23..bc7eee0 100644 --- a/kodereviewer/models/comments.py +++ b/kodereviewer/models/comments.py @@ -60,17 +60,17 @@ class CommentModel(QAbstractListModel): comment: Comment = self._pull_request._comments[index.row()] if role == self.Roles.Body: - return comment.body + return comment._body if role == self.Roles.CreatedAt: - return comment.created_at.strftime("%Y-%M-%d %H:%m") + return comment._created_at.strftime("%Y-%M-%d %H:%m") if role == self.Roles.UpdatedAt: - return comment.updated_at.strftime("%Y-%M-%d %H:%m") + return comment._updated_at.strftime("%Y-%M-%d %H:%m") if role == self.Roles.Username: - return comment.user.username + return comment._user._username if role == self.Roles.AvatarUrl: - return comment.user.avatar_url + return comment._user._avatar_url if role == Qt.ItemDataRole.DisplayRole: - return comment.body + return comment._body return None def rowCount(self, parent: QModelIndex | QPersistentModelIndex = QModelIndex()) -> int: diff --git a/kodereviewer/models/file.py b/kodereviewer/models/file.py index 43fcbe4..c8e4da6 100644 --- a/kodereviewer/models/file.py +++ b/kodereviewer/models/file.py @@ -174,7 +174,7 @@ class TreeFileModel(QAbstractItemModel): def columnCount(self, parent: QModelIndex = QModelIndex()) -> int: return 1 - def data(self, index: QModelIndex, role: int) -> Any: + def data(self, index: QModelIndex | QPersistentModelIndex, role: int = Qt.ItemDataRole.DisplayRole) -> Any: if not index.isValid(): return '' item: FileItem = index.internalPointer() diff --git a/kodereviewer/models/reviews.py b/kodereviewer/models/reviews.py new file mode 100644 index 0000000..81860fc --- /dev/null +++ b/kodereviewer/models/reviews.py @@ -0,0 +1,295 @@ +from dataclasses import dataclass, field +from enum import auto, IntEnum +from logging import getLogger +from typing import Optional, Self, Union + +from PySide6.QtCore import QAbstractItemModel, QAbstractListModel, QByteArray, QModelIndex, QObject, QPersistentModelIndex, QStandardPaths, QUrl, Qt, Signal, Slot, Property +from PySide6.QtQml import QmlElement + +from kodereviewer.data import PullRequest, ReviewComment + +QML_IMPORT_NAME = "org.deprecated.kodereviewer" +QML_IMPORT_MAJOR_VERSION = 1 + +logger = getLogger(__name__) + + +class Roles(IntEnum): + Body = Qt.ItemDataRole.UserRole + 1 + CreatedAt = auto() + UpdatedAt = auto() + Username = auto() + AvatarUrl = auto() + Diff = auto() + Line = auto() + + +# List model + +@dataclass +class Thread(QObject): + id: int + diff: str = '' + reviews: list[ReviewComment] = field(default_factory=list) + + def review_ids(self) -> list[int]: + return [review._id for review in self.reviews] + + +class ReviewModel(QAbstractListModel): + _pull_request: Optional[PullRequest] + threads: list[Thread] + + class Roles(IntEnum): + Id = Qt.ItemDataRole.UserRole + 1 + Diff = auto() + Reviews = auto() + + def __init__(self): + super().__init__() + self._pull_request = None + self.threads = [] + + pullRequestChanged = Signal() + + def get_pull_request(self) -> Optional[PullRequest]: + return self._pull_request + + def set_pull_request(self, pull_request: Optional[PullRequest]) -> None: + if pull_request is None: + return + + if self._pull_request is not None and self._pull_request == pull_request: + return + + self.beginResetModel() + self._pull_request = pull_request + self._pull_request.reviewsLoaded.connect(self._reset_model) + self.endResetModel() + self.pullRequestChanged.emit() + + pullRequest = Property(PullRequest, fget=get_pull_request, fset=set_pull_request, + notify=pullRequestChanged) + + def _reset_model(self): + assert self._pull_request is not None + self.beginResetModel() + self.load_reviews(self._pull_request._reviews) + self.endResetModel() + + def load_reviews(self, reviews: list[ReviewComment]): + self.mapping: dict[int, Thread] = {} + for review in reviews: + logger.info('Processing %d', review.id) + if review._in_reply_to is not None: + thread = self.mapping[review._in_reply_to] + else: + if review.id not in self.mapping: + self.mapping[review._id] = Thread(id=review._id, diff=review._diff) + thread = self.mapping[review._id] + else: + thread = self.mapping[review._id] + + thread.reviews.append(review) + + self.threads = list(self.mapping.values()) + + def data(self, + index: QModelIndex | QPersistentModelIndex, + role: int = Qt.ItemDataRole.DisplayRole) -> object: + + if self._pull_request is None: + return None + + thread = self.threads[index.row()] + + if role == self.Roles.Id: + return thread.id + if role == self.Roles.Diff: + return thread.diff + if role == self.Roles.Reviews: + return thread.reviews + if role == Qt.ItemDataRole.DisplayRole: + return f'Thread #{thread.id}' + + return None + + + def rowCount(self, parent: QModelIndex | QPersistentModelIndex = QModelIndex()) -> int: + if self._pull_request is None: + return 0 + + return len(self.threads) + + def roleNames(self) -> dict[int, QByteArray]: + return { + self.Roles.Id: QByteArray(b'id'), + self.Roles.Diff: QByteArray(b'diff'), + self.Roles.Reviews: QByteArray(b'reviews'), + } + + +# Tree Model + +@dataclass +class ThreadItem: + parent: "Root" + id: int + reviews: list["ReviewItem"] = field(default_factory=list) + + def data(self, role: int): + return None + + +@dataclass +class ReviewItem: + parent: ThreadItem + id: int + review: ReviewComment + + def data(self, role: int): + + if role == Roles.Body: + return self.review._body + if role == Roles.CreatedAt: + return self.review._created_at.strftime("%Y-%M-%d %H:%m") + if role == Roles.UpdatedAt: + return self.review._updated_at.strftime("%Y-%M-%d %H:%m") + if role == Roles.Username: + return self.review._.username + if role == Roles.AvatarUrl: + return self.review._.avatar_url + if role == Roles.Diff: + return self.review.diff + if role == Roles.Line: + return self.review.line + if role == Qt.ItemDataRole.DisplayRole: + return self.review._body + return None + + +@dataclass +class Root: + threads: list[ThreadItem] = field(default_factory=list) + + def data(self, role: int): + return None + + +Item = Union[Root, ThreadItem, ReviewItem] + +@QmlElement +class TreeReviewModel(QAbstractItemModel): + + _pull_request: Optional[PullRequest] + root: Root + + def __init__(self): + super().__init__() + self._pull_request = None + self.root = Root() + + def get_pull_request(self) -> Optional[PullRequest]: + return self._pull_request + + def set_pull_request(self, pull_request: Optional[PullRequest]) -> None: + if pull_request is None: + return + + if self._pull_request is not None and self._pull_request == pull_request: + return + + self.beginResetModel() + self._pull_request = pull_request + self._pull_request.reviewsLoaded.connect(self._reset_model) + self.endResetModel() + self.pullRequestChanged.emit() + + pullRequestChanged = Signal() + pullRequest = Property(PullRequest, fget=get_pull_request, fset=set_pull_request, + notify=pullRequestChanged) + + def load_reviews(self, reviews: list[ReviewComment]): + mapping: dict[int, ThreadItem] = {} + for review in reviews: + if review.id not in mapping: + thread = ThreadItem(self.root, review.id) + else: + thread = mapping[review.id] + thread.reviews.append(ReviewItem(thread, review.id, review)) + + def _reset_model(self) -> None: + if self._pull_request is None: + return + + self.beginResetModel() + self.load_reviews(self._pull_request._reviews) + self.endResetModel() + + def index(self, row: int, column: int, + parent: QModelIndex | QPersistentModelIndex = QModelIndex()) -> QModelIndex: + if not self.hasIndex(row, column, parent): + return QModelIndex() + if not parent.isValid(): + if 0 <= row < len(self.root.threads): + return self.createIndex(row, column, self.root.threads[row]) + else: + item: Root | ThreadItem = parent.internalPointer() + if isinstance(item, Root): + if 0 <= row < len(self.root.threads): + return self.createIndex(row, column, self.root.threads[row]) + else: + if 0 <= row < len(item.reviews): + return self.createIndex(row, column, item.reviews[row]) + return QModelIndex() + + def parent(self, index: QModelIndex) -> QModelIndex: + if not index.isValid(): + return QModelIndex() + item: Item = index.internalPointer() + + if isinstance(item, Root): + return QModelIndex() + if isinstance(item, ThreadItem): + return self.createIndex(self.root.threads.index(item), 0, self.root) + if isinstance(item, ReviewItem): + return self.createIndex(item.parent.reviews.index(item), 0, item.parent) + raise ValueError(f'Unknown {item}') + + def rowCount(self, index: QModelIndex | QPersistentModelIndex = QModelIndex()) -> int: + parent: Item + if not index.isValid(): + return len(self.root.threads) + + parent = index.internalPointer() + + if isinstance(parent, Root): + return len(self.root.threads) + if isinstance(parent, ThreadItem): + return len(parent.reviews) + return 0 + + def columnCount(self, parent: QModelIndex | QPersistentModelIndex = QModelIndex()) -> int: + return 1 + + def data(self, + index: QModelIndex | QPersistentModelIndex, + role: int = Qt.ItemDataRole.DisplayRole) -> object: + if self._pull_request is None: + return None + reviews: list[ReviewComment] = self._pull_request._reviews + review: ReviewComment = reviews[index.row()] + + item: Item = index.internalPointer() + return item.data(role) + + def roleNames(self) -> dict[int, QByteArray]: + return { + Roles.Body: QByteArray(b'body'), + Roles.CreatedAt: QByteArray(b'createdAt'), + Roles.UpdatedAt: QByteArray(b'updatedAt'), + Roles.Username: QByteArray(b'username'), + Roles.AvatarUrl: QByteArray(b'avatarUrl'), + Roles.Diff: QByteArray(b'diff'), + Roles.Line: QByteArray(b'line'), + } diff --git a/kodereviewer/network_manager.py b/kodereviewer/network_manager.py index 3296153..089e69e 100644 --- a/kodereviewer/network_manager.py +++ b/kodereviewer/network_manager.py @@ -18,6 +18,7 @@ PULL_REQUEST_LIST_URL = re.compile(r'/pulls$') COMMENT_LIST_URL = re.compile(r'/issues/(\d+)/comments') FILE_LIST_URL = re.compile(r'/pulls/(\d+)/files') CREATE_REVIEW_URL = re.compile(r'/pulls/(\d+)/reviews') +REVIEW_COMMENTS_URL = re.compile(r'/pulls/(\d+)/comments') logger = logging.getLogger(__name__) @@ -90,6 +91,11 @@ class NetworkManager(QObject): pull_request.load_files(response_body) elif (match := CREATE_REVIEW_URL.search(reply.url().toString())): logger.info(f'Got review reply: {response_body}') + elif (match := REVIEW_COMMENTS_URL.search(reply.url().toString())): + pull_request_number = int(match.groups()[0]) + pull_request: Optional[PullRequest] = self._project.find_pull_request(pull_request_number) + if pull_request is not None: + pull_request.load_reviews(response_body) else: logger.info(f"Can't handle {reply.url()}") @@ -111,6 +117,14 @@ class NetworkManager(QObject): ) ) + @Slot(int) + def getPullRequestReviews(self, pull_request_number: int) -> None: + self._manager.get( + self._request_factory.createRequest( + f'/pulls/{pull_request_number}/comments' + ) + ) + @Slot(int, str, str, str) def createReview( self, pull_request_number: str, commit_id: str, diff --git a/kodereviewer/qml/Editor.qml b/kodereviewer/qml/Editor.qml index 052933d..fdd1cbd 100644 --- a/kodereviewer/qml/Editor.qml +++ b/kodereviewer/qml/Editor.qml @@ -32,10 +32,10 @@ TextEdit { font.family: "monospace" Kirigami.SpellCheck.enabled: false + LineModel { id: lineModel document: root.textDocument - onDocumentChanged: print('Document changed!') } onWidthChanged: lineModel.resetModel() diff --git a/kodereviewer/qml/Main.qml b/kodereviewer/qml/Main.qml index 1c5e951..c1be6b9 100644 --- a/kodereviewer/qml/Main.qml +++ b/kodereviewer/qml/Main.qml @@ -46,8 +46,6 @@ Kirigami.ApplicationWindow { } } - - Loader { id: treeFileModelLoader active: !!pullRequest diff --git a/kodereviewer/qml/MarkdownTextArea.qml b/kodereviewer/qml/MarkdownTextArea.qml index 966b209..9debc1a 100644 --- a/kodereviewer/qml/MarkdownTextArea.qml +++ b/kodereviewer/qml/MarkdownTextArea.qml @@ -10,7 +10,8 @@ QQC2.TextArea { placeholderText: "Leave a comment" wrapMode: TextEdit.Wrap - Kirigami.SpellCheck.enabled: true + Kirigami.SpellCheck.enabled: false + SyntaxHighlighter { id: hightlighter textEdit: root diff --git a/kodereviewer/qml/PullRequestPage.qml b/kodereviewer/qml/PullRequestPage.qml index cdcc1ec..5b52c3e 100644 --- a/kodereviewer/qml/PullRequestPage.qml +++ b/kodereviewer/qml/PullRequestPage.qml @@ -6,6 +6,7 @@ import QtQuick.Layouts import org.kde.kirigami as Kirigami import org.kde.kirigamiaddons.formcard as FormCard +import org.kde.kirigamiaddons.components as KirigamiComponents import org.deprecated.kodereviewer 1.0 @@ -20,6 +21,8 @@ Kirigami.ScrollablePage { property string currentView: "info" + title: pullRequest ? pullRequest.title : "" + actions: [ Kirigami.Action { id: reviewChangesAction @@ -66,6 +69,16 @@ Kirigami.ScrollablePage { } Loader { + id: reviewLoader + active: !!root.pullRequest + sourceComponent: ReviewModel { + id: reviewModel + pullRequest: root.pullRequest + onPullRequestChanged: root.connection.getPullRequestReviews(root.pullRequest.number) + } + } + + Loader { id: fileModelLoader active: !!root.pullRequest sourceComponent: FileModel { @@ -94,34 +107,39 @@ Kirigami.ScrollablePage { Layout.fillHeight: true //anchors.fill: parent } - Kirigami.CardsListView { - visible: !!root.pullRequest && root.currentView == "comments" - //anchors.fill: parent + id: commentsListView Layout.fillWidth: true Layout.fillHeight: true + visible: !!root.pullRequest && root.currentView == "comments" model: commentModelLoader.item delegate: CommentDelegate {} footerPositioning: ListView.OverlayFooter } + ReviewList { + id: reviewListView + visible: !!root.pullRequest && root.currentView == "reviews" + Layout.fillWidth: true + Layout.fillHeight: true + model: reviewLoader.item + } + Editor { + id: editor visible: !!root.pullRequest && root.currentView == "files" - Layout.fillWidth: true Layout.fillHeight: true - id: editor text: "" file: "" - //fileModel: fileModelLoader.item } } Connections { target: contextDrawer function onFileSelected(filename, text) { - print("ASDF") + console.log("file changed!") editor.filename = filename + '.diff' editor.text = text } @@ -132,18 +150,36 @@ Kirigami.ScrollablePage { Kirigami.Action { icon.name: "info" text: i18n("Info") - onTriggered: root.currentView = "info" + onTriggered: { + root.currentView = "info" + //root.flickable = mainLayout + } }, Kirigami.Action { icon.name: "comment-symbolic" text: i18n("Comments") - onTriggered: root.currentView = "comments" + onTriggered: { + root.currentView = "comments" + //root.flickable = commentsListView + } }, Kirigami.Action { icon.name: "file-catalog-symbolic" text: i18n("Files") - onTriggered: root.currentView = "files" + onTriggered: { + root.currentView = "files" + //root.flickable = reviewListView + } + }, + Kirigami.Action { + icon.name: "document-preview-symbolic" + text: i18n("Reviews") + onTriggered: { + root.currentView = "reviews" + + //root.flickable = mainLayout + } } ] } diff --git a/kodereviewer/qml/ReviewList.qml b/kodereviewer/qml/ReviewList.qml new file mode 100644 index 0000000..e8821af --- /dev/null +++ b/kodereviewer/qml/ReviewList.qml @@ -0,0 +1,84 @@ +pragma ComponentBehavior: Bound +import QtQuick +import QtCore +import QtQuick.Controls as QQC2 +import QtQuick.Layouts + +import org.kde.kirigami as Kirigami +import org.kde.kirigamiaddons.formcard as FormCard +import org.kde.kirigamiaddons.components as KirigamiComponents + + +Kirigami.CardsListView { + id: root + + Kirigami.PlaceholderMessage { + visible: root.count == 0 + anchors.centerIn: parent + text: "No reviews!" + } + + delegate: Kirigami.AbstractCard { + id: delegate + + required property int id + required property string diff + required property var reviews + + clip: true + + header: Editor { + text: delegate.diff + file: "bla.txt" + } + + contentItem: Item { + implicitHeight: commentsLayout.implicitHeight + implicitWidth: commentsLayout.implicitWidth + ColumnLayout { + id: commentsLayout + anchors.fill: parent + + Repeater { + model: delegate.reviews + ColumnLayout { + required property var modelData + + Kirigami.Separator { + Layout.fillWidth: true + } + RowLayout { + Layout.fillWidth: true + Layout.fillHeight: true + + KirigamiComponents.Avatar { + name: modelData.user.username + source: modelData.user.avatarUrl + Layout.preferredWidth: Kirigami.Units.iconSizes.smallMedium + Layout.preferredHeight: Kirigami.Units.iconSizes.smallMedium + Layout.fillWidth: false + Layout.alignment: Qt.AlignTop + } + QQC2.Label { + text: `@${modelData.user.username}` + Layout.fillWidth: false + Layout.alignment: Qt.AlignTop + } + } + MarkdownLabel { + Layout.fillWidth: true + text: modelData.body + } + + } + } + + MarkdownTextArea { + id: addComment + Layout.fillWidth: true + } + } + } + } +} + |